Script 'mail_helper' called by obssrc
Hello community,

here is the log from the commit of package rqlite for openSUSE:Factory checked 
in at 2026-05-23 23:23:25
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/rqlite (Old)
 and      /work/SRC/openSUSE:Factory/.rqlite.new.2084 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Package is "rqlite"

Sat May 23 23:23:25 2026 rev:48 rq:1354515 version:10.1.0

Changes:
--------
--- /work/SRC/openSUSE:Factory/rqlite/rqlite.changes    2026-05-14 
21:45:42.257376486 +0200
+++ /work/SRC/openSUSE:Factory/.rqlite.new.2084/rqlite.changes  2026-05-23 
23:23:50.280042869 +0200
@@ -1,0 +2,10 @@
+Thu May 21 19:03:52 UTC 2026 - Andreas Stieger <[email protected]>
+
+- Update to version 10.1.0:
+  * Add Schema management page to Console app
+  * Display node TLS state in console's Cluster panel
+- includes changes from 10.0.6:
+  * Limit number of redirects followed on cluster-join
+  * fix HTTP auth reporting
+
+-------------------------------------------------------------------
@@ -40,0 +51,3 @@
+- includes fix for CVE-2026-33814: golang.org/x/net/http2: infinite
+  loop in HTTP/2 transport when given bad SETTINGS_MAX_FRAME_SIZE
+  (boo#1265706) 

Old:
----
  rqlite-10.0.5.tar.xz

New:
----
  rqlite-10.1.0.tar.xz

++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Other differences:
------------------
++++++ rqlite.spec ++++++
--- /var/tmp/diff_new_pack.oasQbQ/_old  2026-05-23 23:23:51.976112108 +0200
+++ /var/tmp/diff_new_pack.oasQbQ/_new  2026-05-23 23:23:51.976112108 +0200
@@ -17,7 +17,7 @@
 
 
 Name:           rqlite
-Version:        10.0.5
+Version:        10.1.0
 Release:        0
 Summary:        Distributed relational database built on SQLite
 License:        MIT

++++++ _service ++++++
--- /var/tmp/diff_new_pack.oasQbQ/_old  2026-05-23 23:23:52.108117496 +0200
+++ /var/tmp/diff_new_pack.oasQbQ/_new  2026-05-23 23:23:52.128118312 +0200
@@ -3,7 +3,7 @@
     <param name="url">https://github.com/rqlite/rqlite.git</param>
     <param name="scm">git</param>
     <param name="exclude">.git</param>
-    <param name="revision">v10.0.5</param>
+    <param name="revision">v10.1.0</param>
     <param name="versionformat">@PARENT_TAG@</param>
     <param name="changesgenerate">enable</param>
     <param name="versionrewrite-pattern">v(.*)</param>

++++++ _servicedata ++++++
--- /var/tmp/diff_new_pack.oasQbQ/_old  2026-05-23 23:23:52.260123702 +0200
+++ /var/tmp/diff_new_pack.oasQbQ/_new  2026-05-23 23:23:52.272124192 +0200
@@ -1,7 +1,7 @@
 <servicedata>
   <service name="tar_scm">
     <param name="url">https://github.com/rqlite/rqlite.git</param>
-    <param 
name="changesrevision">6aab1c97eddd21fde001aeec50dbf3605ef1069d</param>
+    <param 
name="changesrevision">b8d7a7dc5db9ce2c60ac37d2e0cbbaa2a2ae0811</param>
   </service>
 </servicedata>
 (No newline at EOF)

++++++ rqlite-10.0.5.tar.xz -> rqlite-10.1.0.tar.xz ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/rqlite-10.0.5/CHANGELOG.md 
new/rqlite-10.1.0/CHANGELOG.md
--- old/rqlite-10.0.5/CHANGELOG.md      2026-05-12 18:52:08.000000000 +0200
+++ new/rqlite-10.1.0/CHANGELOG.md      2026-05-20 08:23:17.000000000 +0200
@@ -1,3 +1,17 @@
+## v10.1.0 (May 20th 2026)
+### New features
+- [PR #2672](https://github.com/rqlite/rqlite/pull/2672), [PR 
#2673](https://github.com/rqlite/rqlite/pull/2673): Add Schema management page 
to Console app.
+
+### Implementation changes and bug fixes
+- [PR #2671](https://github.com/rqlite/rqlite/pull/2671): Display node TLS 
state in console's Cluster panel. See [issue 
#2669](https://github.com/rqlite/rqlite/discussions/2669).
+
+## v10.0.6 (May 19th 2026)
+### Implementation changes and bug fixes
+- [PR #2664](https://github.com/rqlite/rqlite/pull/2664): Limit number of 
redirects followed on cluster-join. Fixes issue 
[#2616](https://github.com/rqlite/rqlite/issues/2616). Thanks 
@goingforstudying-ctrl
+- [PR #2667](https://github.com/rqlite/rqlite/pull/2667): Refactor database 
`CheckpointManager` for clarity.
+- [PR #2668](https://github.com/rqlite/rqlite/pull/2668): Move WAL-related 
types to `wal` package.
+- [PR #2670](https://github.com/rqlite/rqlite/pull/2670): Correctly pass a 
`nil` Credential Store to the HTTP service, fixing HTTP auth reporting. See 
[issue #2669](https://github.com/rqlite/rqlite/discussions/2669).
+
 ## v10.0.5 (May 12th 2026)
 ### Implementation changes and bug fixes
 - [PR #2658](https://github.com/rqlite/rqlite/pull/2658): Release images for 
_hard float_ and v6 ARM systems. See [discussion 
#2657](https://github.com/rqlite/rqlite/discussions/2657).
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/rqlite-10.0.5/CONTRIBUTING.md 
new/rqlite-10.1.0/CONTRIBUTING.md
--- old/rqlite-10.0.5/CONTRIBUTING.md   2026-05-12 18:52:08.000000000 +0200
+++ new/rqlite-10.1.0/CONTRIBUTING.md   2026-05-20 08:23:17.000000000 +0200
@@ -7,7 +7,7 @@
 Many packages have their own `DESIGN.md` design document. You should review 
these before making changes. Doing so will help you understand the code and its 
construction.
 
 ## Issues are not assigned
-Issues are never explicitly assigned to inviduals. If you wish to work on 
issue simply ask questions on the issue as needed, and generate a Pull Request 
with your proposed fix.
+Issues are never explicitly assigned to inviduals. If you wish to work on 
issue simply ask questions on the issue as needed, and generate a Pull Request 
with your proposed fix. **Before coding any substantial change it's strongly 
recommended you discuss your proposal first**.
 
 ## Use of Coding Agents
 Coding Agents are fine to use, but any PR that appears to be "AI slop" or 
generated without any apparent thought by the actual programmer, may be closed 
without comment.
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/rqlite-10.0.5/cluster/client.go 
new/rqlite-10.1.0/cluster/client.go
--- old/rqlite-10.0.5/cluster/client.go 2026-05-12 18:52:08.000000000 +0200
+++ new/rqlite-10.1.0/cluster/client.go 2026-05-20 08:23:17.000000000 +0200
@@ -27,6 +27,7 @@
        maxPoolCapacity   = 64
        defaultMaxRetries = 0
        noRetries         = 0
+       maxRedirects      = 10
 
        protoBufferLengthSize = 8
 )
@@ -513,7 +514,10 @@
        if err := ctx.Err(); err != nil {
                return err
        }
-       for {
+       for i := 0; i < maxRedirects; i++ {
+               if err := ctx.Err(); err != nil {
+                       return err
+               }
                conn, err := c.dial(nodeAddr)
                if err != nil {
                        return err
@@ -558,6 +562,7 @@
                }
                return nil
        }
+       return errors.New("max redirects exceeded")
 }
 
 // BroadcastHWM performs a broadcast to all specified nodes.
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/rqlite-10.0.5/cluster/client_test.go 
new/rqlite-10.1.0/cluster/client_test.go
--- old/rqlite-10.0.5/cluster/client_test.go    2026-05-12 18:52:08.000000000 
+0200
+++ new/rqlite-10.1.0/cluster/client_test.go    2026-05-20 08:23:17.000000000 
+0200
@@ -340,6 +340,160 @@
        }
 }
 
+func Test_ClientJoinNode_MaxRedirectsExceeded(t *testing.T) {
+       // Simulate a cluster where every node always redirects to another node,
+       // creating an infinite redirect loop. The client should stop after
+       // maxRedirects and return an error.
+       redirects := make(chan struct{}, maxRedirects)
+
+       srv := servicetest.NewService()
+       srv.Handler = func(conn net.Conn) {
+               redirects <- struct{}{}
+
+               c := readCommand(conn)
+               if c == nil {
+                       return
+               }
+               if c.Type != proto.Command_COMMAND_TYPE_JOIN {
+                       t.Fatalf("unexpected command type: %d", c.Type)
+               }
+
+               // Always respond with "not leader" and a leader address to 
redirect to.
+               p, err := pb.Marshal(&proto.CommandJoinResponse{
+                       Error:  "not leader",
+                       Leader: srv.Addr(), // redirect back to same server to 
simulate loop
+               })
+               if err != nil {
+                       conn.Close()
+                       return
+               }
+               writeBytesWithLength(conn, p)
+       }
+       srv.Start()
+       defer srv.Close()
+
+       c := NewClient(&simpleDialer{}, 0)
+       req := &command.JoinRequest{
+               Address: "test-node-addr",
+       }
+       err := c.Join(context.Background(), req, srv.Addr(), nil, time.Second)
+       if err == nil {
+               t.Fatal("expected error when max redirects exceeded")
+       }
+       if !strings.Contains(err.Error(), "max redirects exceeded") {
+               t.Fatalf("expected 'max redirects exceeded' error, got: %s", 
err)
+       }
+
+       close(redirects)
+       count := 0
+       for range redirects {
+               count++
+       }
+       if count != maxRedirects {
+               t.Fatalf("expected %d redirect attempts, got %d", maxRedirects, 
count)
+       }
+}
+
+func Test_ClientJoinNode_ContextCanceledDuringRedirect(t *testing.T) {
+       // Simulate a redirect loop and cancel the context partway through.
+       // The context check happens at the top of each loop iteration, before
+       // dialing. Because the local test server responds instantly, we need
+       // to cancel the context before calling Join to guarantee the
+       // cancellation is observed before the loop exhausts maxRedirects.
+       srv := servicetest.NewService()
+       srv.Handler = func(conn net.Conn) {
+               c := readCommand(conn)
+               if c == nil {
+                       return
+               }
+               if c.Type != proto.Command_COMMAND_TYPE_JOIN {
+                       t.Fatalf("unexpected command type: %d", c.Type)
+               }
+
+               p, err := pb.Marshal(&proto.CommandJoinResponse{
+                       Error:  "not leader",
+                       Leader: srv.Addr(),
+               })
+               if err != nil {
+                       conn.Close()
+                       return
+               }
+               writeBytesWithLength(conn, p)
+       }
+       srv.Start()
+       defer srv.Close()
+
+       c := NewClient(&simpleDialer{}, 0)
+       req := &command.JoinRequest{
+               Address: "test-node-addr",
+       }
+
+       ctx, cancel := context.WithCancel(context.Background())
+       cancel() // cancel immediately so the first iteration sees it
+
+       err := c.Join(ctx, req, srv.Addr(), nil, time.Second)
+       if err == nil {
+               t.Fatal("expected error when context is canceled")
+       }
+       if !strings.Contains(err.Error(), "context canceled") {
+               t.Fatalf("expected context canceled error, got: %s", err)
+       }
+}
+
+func Test_ClientJoinNode_SuccessAfterRedirect(t *testing.T) {
+       // First response redirects, second response succeeds.
+       attempts := make(chan struct{}, 2)
+
+       srv := servicetest.NewService()
+       srv.Handler = func(conn net.Conn) {
+               attempts <- struct{}{}
+
+               c := readCommand(conn)
+               if c == nil {
+                       return
+               }
+               if c.Type != proto.Command_COMMAND_TYPE_JOIN {
+                       t.Fatalf("unexpected command type: %d", c.Type)
+               }
+
+               var p []byte
+               var err error
+               if len(attempts) == 1 {
+                       p, err = pb.Marshal(&proto.CommandJoinResponse{
+                               Error:  "not leader",
+                               Leader: srv.Addr(),
+                       })
+               } else {
+                       p, err = pb.Marshal(&proto.CommandJoinResponse{})
+               }
+               if err != nil {
+                       conn.Close()
+                       return
+               }
+               writeBytesWithLength(conn, p)
+       }
+       srv.Start()
+       defer srv.Close()
+
+       c := NewClient(&simpleDialer{}, 0)
+       req := &command.JoinRequest{
+               Address: "test-node-addr",
+       }
+       err := c.Join(context.Background(), req, srv.Addr(), nil, time.Second)
+       if err != nil {
+               t.Fatalf("expected success after redirect, got: %s", err)
+       }
+
+       close(attempts)
+       count := 0
+       for range attempts {
+               count++
+       }
+       if count != 2 {
+               t.Fatalf("expected 2 attempts, got %d", count)
+       }
+}
+
 func Test_ClientRetry_SuccessFirstAttempt(t *testing.T) {
        srv := servicetest.NewService()
        srv.Handler = func(conn net.Conn) {
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/rqlite-10.0.5/cmd/rqlited/main.go 
new/rqlite-10.1.0/cmd/rqlited/main.go
--- old/rqlite-10.0.5/cmd/rqlited/main.go       2026-05-12 18:52:08.000000000 
+0200
+++ new/rqlite-10.1.0/cmd/rqlited/main.go       2026-05-20 08:23:17.000000000 
+0200
@@ -437,7 +437,11 @@
 
 func startHTTPService(cfg *Config, str *store.Store, cltr *cluster.Client, 
credStr *auth.CredentialsStore, pxy *proxy.Proxy) (*httpd.Service, error) {
        // Create HTTP server and load authentication information.
-       s := httpd.New(cfg.HTTPAddr, str, cltr, pxy, credStr)
+       var cs httpd.CredentialStore
+       if credStr != nil {
+               cs = credStr
+       }
+       s := httpd.New(cfg.HTTPAddr, str, cltr, pxy, cs)
 
        s.CACertFile = cfg.HTTPx509CACert
        s.CertFile = cfg.HTTPx509Cert
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/rqlite-10.0.5/db/checkpoint_manager.go 
new/rqlite-10.1.0/db/checkpoint_manager.go
--- old/rqlite-10.0.5/db/checkpoint_manager.go  2026-05-12 18:52:08.000000000 
+0200
+++ new/rqlite-10.1.0/db/checkpoint_manager.go  2026-05-20 08:23:17.000000000 
+0200
@@ -1,7 +1,6 @@
 package db
 
 import (
-       "encoding/binary"
        "expvar"
        "fmt"
        "io"
@@ -61,12 +60,10 @@
        dbPath  string
        walPath string
 
-       salt *[2]uint32
-
-       // nextFrameIdx is the index of the next frame in the WAL file to read 
as
-       // part of a checkpoint attempt. This value is 0-based in the sense that
-       // if the first frame is to be read, then this value is 0, not 1.
-       nextFrameIdx int64
+       // resetWatch carries forward state from a partial-checkpoint (all pages
+       // moved but WAL not truncated) so the next attempt can detect whether
+       // SQLite reset the WAL in the meantime and decide where to resume.
+       resetWatch *WALResetWatch
 
        logger *log.Logger
 }
@@ -74,10 +71,11 @@
 // NewCheckpointManager returns a new CheckpointManager for the given database.
 func NewCheckpointManager(db *DB) (*CheckpointManager, error) {
        return &CheckpointManager{
-               db:      db,
-               dbPath:  db.Path(),
-               walPath: db.WALPath(),
-               logger:  log.New(log.Writer(), "[db-checkpoint] ", 
log.LstdFlags),
+               db:         db,
+               dbPath:     db.Path(),
+               resetWatch: &WALResetWatch{},
+               walPath:    db.WALPath(),
+               logger:     log.New(log.Writer(), "[db-checkpoint] ", 
log.LstdFlags),
        }, nil
 }
 
@@ -116,8 +114,7 @@
        stats.Get(preCompactWALSize).(*expvar.Int).Set(walSzPre)
 
        if walSzPre == 0 {
-               cm.nextFrameIdx = 0
-               cm.salt = nil
+               cm.resetWatch.Disarm()
                return &CheckpointManagerMeta{}, 0, nil
        }
 
@@ -131,8 +128,7 @@
                if !meta.Success() {
                        return mmeta, 0, fmt.Errorf("checkpoint did not 
complete within %s", timeout)
                }
-               cm.nextFrameIdx = 0
-               cm.salt = nil
+               cm.resetWatch.Disarm()
                return mmeta, 0, nil
        }
 
@@ -144,31 +140,17 @@
        }
        defer walFD.Close()
 
-       walReset := false
-       if cm.nextFrameIdx > 0 {
-               // The manager is telling us to start reading from other than 
the start. This
-               // means that the all frames were moved in the last checkpoint 
attempt, but the
-               // truncate itself failed. Before we start reading the WAL file 
from the given
-               // frame index, we need to check if the WAL was reset. If it 
was reset then
-               // SQLite started writing new frames from the beginning of the 
file, not appending
-               // at index nextFrameIdx.
-               //
-               // We can perform this check by comparing the salt values. If 
the salt values
-               // do not match, then we know that the WAL file has been reset, 
and we need to
-               // reset our state to start reading from the beginning of the 
WAL file again.
-               salt, err := readSaltAt(walFD)
-               if err != nil {
-                       return nil, 0, fmt.Errorf("read WAL salt: %w", err)
-               }
-               if cm.salt == nil || *salt != *cm.salt {
-                       cm.nextFrameIdx = 0
-                       cm.salt = salt
-                       walReset = true
-               }
+       // Record the salt in the WAL header before any changes take place. If 
we
+       // were watching for a WAL reset, the watch tells us where to resume and
+       // whether the WAL was in fact reset since it was armed.
+       preChkSalt, err := wal.ReadSaltAt(walFD)
+       if err != nil {
+               return nil, 0, fmt.Errorf("read WAL salt: %w", err)
        }
+       startFrameIdx, walReset := cm.resetWatch.Check(preChkSalt)
 
        compactStartTime := time.Now()
-       scanner, err := wal.NewCompactingFrameScanner(walFD, cm.nextFrameIdx, 
false)
+       scanner, err := wal.NewCompactingFrameScanner(walFD, startFrameIdx, 
false)
        if err != nil {
                return nil, 0, fmt.Errorf("create compacting frame scanner: 
%w", err)
        }
@@ -184,14 +166,11 @@
        stats.Get(compactedWALSize).(*expvar.Int).Set(n)
 
        
/////////////////////////////////////////////////////////////////////////////////
-       // Now, attempt to perform a TRUNCATE checkpoint of the database.
-
-       // Grab salt state before checkpoint is attempted.
-       cm.salt, err = readSaltAt(walFD)
-       if err != nil {
-               return nil, 0, fmt.Errorf("read WAL salt: %w", err)
+       // Now, attempt to perform a TRUNCATE checkpoint of the database. Close 
the WAL
+       // file handle explicitly to avoid any chance of intefering with SQLite.
+       if err := walFD.Close(); err != nil {
+               return nil, 0, fmt.Errorf("create WAL writer: %w", err)
        }
-
        meta, err := cm.db.CheckpointWithTimeout(CheckpointTruncate, timeout)
        if err != nil {
                return nil, 0, fmt.Errorf("checkpoint: %w", err)
@@ -205,8 +184,7 @@
        if rc == 0 {
                // WAL was reset. Next write will start at the beginning of the 
WAL file.
                stats.Add(numCheckpointWALTruncated, 1)
-               cm.nextFrameIdx = 0
-               cm.salt = nil
+               cm.resetWatch.Disarm()
                return mmeta, n, nil
        }
        if pnCkpt < pnLog {
@@ -221,14 +199,13 @@
                // file not truncated. We can use the WAL data, but it requires 
special
                // handling.
                //
-               // This needs to be handled carefully because we do not know 
where the next
-               // WAL frame will be written. That is only revealed when the 
next write takes
-               // place. It might be written to the end of WAL file, or SQLite 
might reset
-               // the WAL, which would cause the next WAL frame to be written 
at the beginning
-               // of the file. The only way to tell will be to check the salt 
values on the
-               // next checkpoint attempt.
+               // We do not know where the next WAL frame will be written. 
That is only
+               // revealed when the next write takes place: SQLite might 
append at the end
+               // of the WAL file, or it might reset the WAL and write from 
the start. The
+               // only way to tell on the next checkpoint attempt is to 
compare salt
+               // values, so arm the watch with the salt and resume frame we 
will need.
                stats.Add(numCheckpointPartial, 1)
-               cm.nextFrameIdx = int64(pnCkpt)
+               cm.resetWatch.Arm(preChkSalt, int64(pnCkpt))
                return mmeta, 0, nil
        }
        stats.Add(numCheckpointInvariantErrors, 1)
@@ -239,15 +216,3 @@
 func (cm *CheckpointManager) Close() error {
        return nil
 }
-
-// readSaltAt reads the salt values from the WAL header at the given ReaderAt.
-func readSaltAt(r io.ReaderAt) (*[2]uint32, error) {
-       buf := make([]byte, 8)
-       if _, err := r.ReadAt(buf, 16); err != nil {
-               return nil, err
-       }
-       return &[2]uint32{
-               binary.BigEndian.Uint32(buf[0:]),
-               binary.BigEndian.Uint32(buf[4:]),
-       }, nil
-}
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/rqlite-10.0.5/db/checkpoint_manager_test.go 
new/rqlite-10.1.0/db/checkpoint_manager_test.go
--- old/rqlite-10.0.5/db/checkpoint_manager_test.go     2026-05-12 
18:52:08.000000000 +0200
+++ new/rqlite-10.1.0/db/checkpoint_manager_test.go     2026-05-20 
08:23:17.000000000 +0200
@@ -513,11 +513,11 @@
        if meta.WALReset {
                t.Fatal("first checkpoint: WALReset must be false on the very 
first attempt")
        }
-       if cm.nextFrameIdx == 0 {
-               t.Fatalf("first checkpoint: expected nextFrameIdx > 0 (pnCkpt 
== pnLog path), got 0 (meta=%s)", meta)
+       if !cm.resetWatch.armed {
+               t.Fatal("first checkpoint: expected resetWatch to be armed 
(pnCkpt == pnLog path)")
        }
-       if cm.salt == nil {
-               t.Fatal("first checkpoint: expected salt to be saved")
+       if cm.resetWatch.resumeFrameIdx == 0 {
+               t.Fatalf("first checkpoint: expected resumeFrameIdx > 0 (pnCkpt 
== pnLog path), got 0 (meta=%s)", meta)
        }
 
        // Release the reader. The previous checkpoint moved all frames to the
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/rqlite-10.0.5/db/db.go new/rqlite-10.1.0/db/db.go
--- old/rqlite-10.0.5/db/db.go  2026-05-12 18:52:08.000000000 +0200
+++ new/rqlite-10.1.0/db/db.go  2026-05-20 08:23:17.000000000 +0200
@@ -210,8 +210,15 @@
 
 // CheckpointMeta contains metadata about a WAL checkpoint operation.
 type CheckpointMeta struct {
-       Code  int
+       // Code is the return code for the operation. Zero if successful.
+       Code int
+
+       // Pages is the total number of frames in the WAL.
        Pages int
+
+       // Moved is the total number of checkpointed frames in the log file
+       // (including any that were already checkpointed before Checkpoint
+       // was called)
        Moved int
 }
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/rqlite-10.0.5/db/db_test.go 
new/rqlite-10.1.0/db/db_test.go
--- old/rqlite-10.0.5/db/db_test.go     2026-05-12 18:52:08.000000000 +0200
+++ new/rqlite-10.1.0/db/db_test.go     2026-05-20 08:23:17.000000000 +0200
@@ -94,7 +94,7 @@
        }
 }
 
-// Test_WALNotCheckpointedOnClose tests that when a database with an existing
+// Test_WALNotChangedOnReopen tests that when a database with an existing
 // file is opened, that the files are not modified in anyway.
 func Test_WALNotChangedOnReopen(t *testing.T) {
        path := mustTempPath()
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/rqlite-10.0.5/db/wal/compacting_section_scanner_test.go 
new/rqlite-10.1.0/db/wal/compacting_section_scanner_test.go
--- old/rqlite-10.0.5/db/wal/compacting_section_scanner_test.go 2026-05-12 
18:52:08.000000000 +0200
+++ new/rqlite-10.1.0/db/wal/compacting_section_scanner_test.go 2026-05-20 
08:23:17.000000000 +0200
@@ -357,9 +357,11 @@
        defer srcConn.Close()
        mustExec(srcConn, "PRAGMA wal_autocheckpoint=0")
        mustExec(srcConn, "CREATE TABLE foo (id INTEGER PRIMARY KEY, name 
TEXT)")
+       mustExec(srcConn, "CREATE TABLE bar (id INTEGER PRIMARY KEY, name 
TEXT)")
 
        // Insert rows to generate WAL frames.
-       for i := range 100 {
+       testCount := 1000
+       for i := range testCount {
                mustExec(srcConn, fmt.Sprintf("INSERT INTO foo (name) VALUES 
('row%d')", i))
        }
 
@@ -373,6 +375,7 @@
        if err != nil {
                t.Fatal(err)
        }
+       t.Logf("source WAL is %d bytes in size", mustFileSize(srcWAL))
 
        s, err := NewCompactingFrameScanner(bytes.NewReader(walBytes), 0, false)
        if err != nil {
@@ -395,6 +398,7 @@
        if _, err := w.WriteTo(destF); err != nil {
                t.Fatal(err)
        }
+       t.Logf("compacted WAL is %d bytes in size", mustFileSize(destWAL))
 
        // Open the dest database and verify data is present.
        destDSN := fmt.Sprintf("file:%s", destDB)
@@ -404,12 +408,19 @@
        }
        defer destConn.Close()
 
+       // Check that inserted record counts are correct for both tables.
        var count int
        if err := destConn.QueryRow("SELECT COUNT(*) FROM foo").Scan(&count); 
err != nil {
                t.Fatal(err)
        }
-       if count != 100 {
-               t.Fatalf("expected 100 rows, got %d", count)
+       if count != testCount {
+               t.Fatalf("expected %d rows, got %d", testCount, count)
+       }
+       if err := destConn.QueryRow("SELECT COUNT(*) FROM bar").Scan(&count); 
err != nil {
+               t.Fatal(err)
+       }
+       if count != 0 {
+               t.Fatalf("expected 0 rows, got %d", count)
        }
 }
 
@@ -691,3 +702,11 @@
                t.Fatalf("failed to iterate rows: %s", err)
        }
 }
+
+func mustFileSize(path string) int64 {
+       stat, err := os.Stat(path)
+       if err != nil {
+               panic("failed to stat file")
+       }
+       return stat.Size()
+}
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/rqlite-10.0.5/db/wal/salt.go 
new/rqlite-10.1.0/db/wal/salt.go
--- old/rqlite-10.0.5/db/wal/salt.go    1970-01-01 01:00:00.000000000 +0100
+++ new/rqlite-10.1.0/db/wal/salt.go    2026-05-20 08:23:17.000000000 +0200
@@ -0,0 +1,32 @@
+package wal
+
+import (
+       "encoding/binary"
+       "fmt"
+       "io"
+)
+
+// Salt represents the two 32-bit salt values from a SQLite WAL header.
+type Salt [2]uint32
+
+// Equal reports whether s and other hold the same salt values.
+func (s Salt) Equal(other Salt) bool {
+       return s == other
+}
+
+// String returns a human-readable representation of s.
+func (s Salt) String() string {
+       return fmt.Sprintf("Salt(%d,%d)", s[0], s[1])
+}
+
+// ReadSaltAt reads the salt values from the WAL header via the given ReaderAt.
+func ReadSaltAt(r io.ReaderAt) (Salt, error) {
+       var buf [8]byte
+       if _, err := r.ReadAt(buf[:], 16); err != nil {
+               return Salt{}, err
+       }
+       return Salt{
+               binary.BigEndian.Uint32(buf[0:4]),
+               binary.BigEndian.Uint32(buf[4:8]),
+       }, nil
+}
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/rqlite-10.0.5/db/wal_reset_watch.go 
new/rqlite-10.1.0/db/wal_reset_watch.go
--- old/rqlite-10.0.5/db/wal_reset_watch.go     1970-01-01 01:00:00.000000000 
+0100
+++ new/rqlite-10.1.0/db/wal_reset_watch.go     2026-05-20 08:23:17.000000000 
+0200
@@ -0,0 +1,47 @@
+package db
+
+import "github.com/rqlite/rqlite/v10/db/wal"
+
+// WALResetWatch records state carried forward from a checkpoint attempt
+// that moved all pages from the WAL to the database but failed to truncate
+// the WAL itself. In that situation SQLite may, on the next write, either
+// append to the existing WAL or reset it from the start — and the only way
+// to tell which happened is to compare the WAL header's salt against the
+// salt observed at the time of the partial-checkpoint.
+//
+// While armed the watch carries the salt to compare against and the frame
+// index to resume reading from if the WAL has not been reset. The zero
+// value is the unarmed state; all methods are safe on the zero value.
+type WALResetWatch struct {
+       armed          bool
+       salt           wal.Salt
+       resumeFrameIdx int64
+}
+
+// Arm starts the watch, recording the salt observed and the frame index
+// from which the next checkpoint should resume if the WAL has not been
+// reset in the meantime.
+func (w *WALResetWatch) Arm(s wal.Salt, resumeFrameIdx int64) {
+       *w = WALResetWatch{armed: true, salt: s, resumeFrameIdx: resumeFrameIdx}
+}
+
+// Disarm clears the watch.
+func (w *WALResetWatch) Disarm() {
+       *w = WALResetWatch{}
+}
+
+// Check returns the frame index at which the next checkpoint should begin
+// reading the WAL, and whether a WAL reset was detected since the watch
+// was armed. When the watch is not armed it returns (0, false). On reset
+// detection the watch is disarmed: the prior resume frame index refers to
+// a WAL state that no longer exists.
+func (w *WALResetWatch) Check(current wal.Salt) (frameIdx int64, walReset 
bool) {
+       if !w.armed {
+               return 0, false
+       }
+       if w.salt.Equal(current) {
+               return w.resumeFrameIdx, false
+       }
+       w.Disarm()
+       return 0, true
+}
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/rqlite-10.0.5/http/console/index.html 
new/rqlite-10.1.0/http/console/index.html
--- old/rqlite-10.0.5/http/console/index.html   2026-05-12 18:52:08.000000000 
+0200
+++ new/rqlite-10.1.0/http/console/index.html   2026-05-20 08:23:17.000000000 
+0200
@@ -12,6 +12,7 @@
         <div class="header-content">
             <h1 id="home-link" class="home-link"><svg class="logo" viewBox="0 
0 46.214 44.45" xmlns="http://www.w3.org/2000/svg"; 
xmlns:xlink="http://www.w3.org/1999/xlink";><image width="46.214" height="44.45" 
preserveAspectRatio="none" 
xlink:href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAIMAAAB+CAYAAAAHkaKhAAAABmJLR0QA/wD/AP+gvaeTAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAB3RJTUUH4AQbASMgLA364gAABWlJREFUeNrt3c9PXFUYxvFnDszAtDNhbBkqqf0BNBVTEwuxEBe0YNKwMVZr4kLd+A+oMUZafyytsjO6swvXutESEwFN1IVWMSYuSlAjLYImFkwsggwzlLkuDKaYGJm599zzvuc+3w2ruRyYT957584ZABhjjDHGGGOMMcYYY4wxxhhjzGUpLQudvDwfXHx/Zsc/VK5jP1Yampyu+fnT7ThxOKfmd9yoYZEffv5TcObZcaxXNnf8mKamBWTuPuoUxMczy5i6thL0deRVgDAaIJx9bqImCABQLm+gcuUH5DfLztZ+o7SJ4Te/x9TcSsDThCMI2ydE2vmEKGQbMPHUneg7LHtCGJ8h/DMhpgVMiDfkT4iUzxC2TYjmNNLHjmLV8YT46Olu3HtI5kWlSQIEACivb2BDwIR4bGwVU9eDgBgcQbgVhPNTRgUY/qAqEoSRBOFhixC2gbhCEGIxbEEoW4Yg6mWnQBAmaRAIQigGVxAIQhgG1xBE3YcQAsIkGQJfZTjGIA0CQTjCIBUCQcSMQToEgogJgxYISQdhCIEgYsGgFUJSQRhCIAirGHyBkDQQkW+IHf9iPnh0ZBLVaoB0o5sbnAM
 
97Xjywe7Ij9vUnMaFy6v49uc15yD69qUi3yCT8m0inOptx6cXH7K2k6hys/pa/+j0SL0gikcOYWlXMdQaChlg4gGDqEEYQqitTKM599XIsdHjd+zy7pRhCIEgIsOQNAg+gwiFYfLLheCsYwgnHUCQCOKbxfAgQmF4d/JH63sW/28ifOYIgjQQl+YCt5PBZaccTgSJIES9mkgqBJ9AGEIgCJUYJEPwAYQhBIJQhUETBM0gDCEQhAoMmiFoBNFICPGAqNyson90euQXYqitkx5B+DeI0++sjCz9IXPEiztN5Fpb8PgT/fCxTKM5N3AkH/lxe/YCj3R6hiHX2oJyVyc2ghTYziG81AukjUcYCMEtBDEYCKH2jkcMQQSGXGsLKoTgHIJzDFsQKoRQM4SMhWfOEAIhOMWQLxKCNAhOMOSLLSh3EkLNEHrsQogdAyGEgNBg/3sZQiCEWDEQQu3dEzOEWDAQQn0QXo4ZgnUMhKAHglUMhKALgjUMhKAPghUMhKATQuQYCEEvhEgxEIJuCJFhIIQ6IOyRBSESDIRQJ4ReWRCAkLujd7e2oNyZjRxCEPgL4a5CgB6BEEJPhj/zBSsT4a2ZAFeX/SSxt0kmBGv3GcK2sAoMjVW9AzE+txlsVOWuT+zH6+Y9AyEdgmgMPoHQAEE8Bh9AaIGgAoNmEJogqMGgEYQ2CKow3ApiVjgIjRDUYdgCcb9gEFohqMQgGYRmCGoxSAShHUJoDOeH23HgtkziQbiGkE8D3XtMyimGrrZs6pNnuhMNQgKEwQMNKeeTIekg
 
fIIQ2TVDEkG4hpCLGEKkF5BJAiEBwlDEEICI/3sdAMwuloKh17/Dwu+Vuo9R6DiIG/m2uh9/MAe8PZhCMRv9E7G4FsAlhN1pYGB/g5VtZVYOGhZEWAwAcHsWeKUPKDbDm2xNBKv3GSScMn4tAS9OAb+tE4JTDJJAnJ8CFkuE4BSDFBDXS8ALX+sFERcE6xikgFhUCiJOCLF
 
gIAgdEGLDQBC1lXcAIVYMBLFzCIMOIMSOgSDkQnCCgSBkQnCGgSDkQXCKgSBkQXCOIekgJEEQgSGpIKRBEIMhaSAkQgAsvYUdptmlUnDmvTVMbxacraEtC7xq6e1vqRBEYgCA2eUgGBqrYmEVTkFcOPH31yRAEIvBRxDSIYjG4BMIDRDEYfABhBYIKjBoBqEJghoMWyAuXXP7scq2bAr37dv5GroKhn8ckzHGGGOMMcYYY4wxxhhj7D/6C6s6jhtV+7AcAAAAAElFTkSuQmCC"/></svg>
 rqlite</h1>
             <nav>
+                <button class="tab" data-tab="schema">Schema</button>
                 <button class="tab" data-tab="query">Query</button>
                 <button class="tab" data-tab="backup">Backup</button>
             </nav>
@@ -26,6 +27,22 @@
                 <div class="query-actions">
                     <button id="execute-btn">Execute</button>
                     <span class="hint">Ctrl+Enter to execute</span>
+                    <label class="query-option query-option-right" title="Read 
consistency level for queries; ignored for writes">
+                        Level
+                        <select id="query-level">
+                            <option value="weak">weak (default)</option>
+                            <option value="linearizable">linearizable</option>
+                            <option value="none">none</option>
+                            <option value="strong">strong</option>
+                        </select>
+                    </label>
+                    <label class="query-option query-freshness-field" 
title="Maximum staleness — Go duration (e.g. 5s, 1m). Only meaningful with 
level=none.">
+                        Freshness
+                        <input type="text" id="query-freshness" 
placeholder="e.g. 5s">
+                    </label>
+                    <label class="query-option query-freshness-field" 
title="Reject the query if data freshness can't be verified within the window">
+                        <input type="checkbox" id="query-freshness-strict"> 
Strict
+                    </label>
                 </div>
             </div>
             <div id="query-history" class="query-history"></div>
@@ -51,6 +68,13 @@
                 <pre id="raw-json" class="raw-json"></pre>
             </div>
         </section>
+        <section id="schema" class="tab-content">
+            <div class="schema-controls">
+                <button id="schema-refresh-btn">Refresh</button>
+                <span id="schema-last-updated" class="last-updated"></span>
+            </div>
+            <div id="schema-content"></div>
+        </section>
         <section id="backup" class="tab-content">
             <div class="backup-panel">
                 <h3>Download Backup</h3>
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/rqlite-10.0.5/http/console/static/css/style.css 
new/rqlite-10.1.0/http/console/static/css/style.css
--- old/rqlite-10.0.5/http/console/static/css/style.css 2026-05-12 
18:52:08.000000000 +0200
+++ new/rqlite-10.1.0/http/console/static/css/style.css 2026-05-20 
08:23:17.000000000 +0200
@@ -258,8 +258,46 @@
 .query-actions {
     display: flex;
     align-items: center;
-    gap: 1rem;
+    gap: 0.75rem;
     margin-top: 0.5rem;
+    flex-wrap: wrap;
+}
+
+.query-option {
+    display: inline-flex;
+    align-items: center;
+    gap: 0.375rem;
+    font-size: 0.8125rem;
+    color: var(--text-secondary);
+    cursor: pointer;
+    user-select: none;
+}
+
+.query-option-right {
+    margin-left: auto;
+}
+
+.query-option select,
+.query-option input[type="text"] {
+    background: var(--surface);
+    color: var(--text);
+    border: 1px solid var(--border);
+    border-radius: 3px;
+    padding: 0.25rem 0.375rem;
+    font-size: 0.8125rem;
+    font-family: inherit;
+}
+
+.query-option input[type="text"] {
+    width: 5rem;
+}
+
+.query-freshness-field {
+    display: none;
+}
+
+.query-actions.show-freshness .query-freshness-field {
+    display: inline-flex;
 }
 
 #execute-btn {
@@ -437,6 +475,214 @@
     color: var(--accent);
 }
 
+/* Schema Tab */
+.schema-controls {
+    display: flex;
+    align-items: center;
+    gap: 1rem;
+    margin-bottom: 1rem;
+}
+
+.schema-loading,
+.schema-empty {
+    color: var(--text-muted);
+    font-size: 0.875rem;
+    padding: 1rem 0;
+}
+
+.schema-actions {
+    display: flex;
+    justify-content: flex-end;
+    gap: 0.5rem;
+    margin-bottom: 0.5rem;
+}
+
+.schema-actions button {
+    background: transparent;
+    border: 1px solid var(--border);
+    color: var(--text-secondary);
+    font-size: 0.75rem;
+    padding: 0.25rem 0.625rem;
+    border-radius: 3px;
+    cursor: pointer;
+}
+
+.schema-actions button:hover {
+    background: var(--surface-alt);
+    color: var(--text);
+}
+
+.schema-count-toggle {
+    display: inline-flex;
+    align-items: center;
+    gap: 0.375rem;
+    margin-left: 0.5rem;
+    font-size: 0.8125rem;
+    color: var(--text-secondary);
+    cursor: pointer;
+    user-select: none;
+}
+
+.schema-section .detail-section-header {
+    font-family: "SF Mono", "Fira Code", "Fira Mono", Menlo, Consolas, 
monospace;
+}
+
+.schema-kind {
+    display: inline-block;
+    font-size: 0.6875rem;
+    text-transform: uppercase;
+    letter-spacing: 0.04em;
+    padding: 0.0625rem 0.375rem;
+    margin-right: 0.375rem;
+    border-radius: 3px;
+    background: var(--surface-alt);
+    color: var(--text-secondary);
+    font-weight: 600;
+    vertical-align: middle;
+}
+
+.schema-count {
+    color: var(--text-muted);
+    font-weight: normal;
+    font-size: 0.8125rem;
+}
+
+.schema-rowcount-text {
+    text-decoration: underline dotted;
+    text-underline-offset: 2px;
+    cursor: help;
+}
+
+.schema-section .detail-section-body {
+    padding: 0;
+}
+
+.schema-columns {
+    border-radius: 0;
+    box-shadow: none;
+}
+
+
+.result-table th.schema-center,
+.result-table td.schema-center {
+    text-align: center;
+}
+
+.schema-check {
+    color: var(--success-text);
+    font-weight: 700;
+    font-size: 1.5rem;
+    line-height: 1;
+}
+
+.schema-dash {
+    color: var(--text-muted);
+}
+
+.schema-pk-badge {
+    font-size: 0.75rem;
+    margin-left: 0.25rem;
+}
+
+.schema-pk-row td:first-child {
+    font-weight: 600;
+}
+
+.schema-sql-block {
+    padding: 0.5rem 0.75rem 0.75rem;
+    border-top: 1px solid var(--border-light);
+    background: var(--surface);
+}
+
+.schema-sql-actions {
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    gap: 0.5rem;
+}
+
+.schema-sql-toggle {
+    background: transparent;
+    border: 1px solid var(--border);
+    color: var(--text-secondary);
+    font-size: 0.75rem;
+    padding: 0.25rem 0.625rem;
+    border-radius: 3px;
+    cursor: pointer;
+}
+
+.schema-sql-toggle:hover {
+    background: var(--surface-alt);
+    color: var(--text);
+}
+
+.schema-drop-table {
+    background: transparent;
+    border: 1px solid var(--error);
+    color: var(--error-text);
+    font-size: 0.75rem;
+    padding: 0.25rem 0.625rem;
+    border-radius: 3px;
+    cursor: pointer;
+    font-weight: 600;
+}
+
+.schema-drop-table:hover {
+    background: var(--error);
+    color: #fff;
+}
+
+.schema-drop-table:disabled {
+    opacity: 0.5;
+    cursor: not-allowed;
+}
+
+.schema-sql-wrapper {
+    position: relative;
+    margin-top: 0.5rem;
+}
+
+.schema-sql-wrapper.hidden {
+    display: none;
+}
+
+.schema-sql {
+    margin: 0;
+    padding: 0.625rem 2.25rem 0.625rem 0.75rem;
+    background: var(--surface-alt);
+    border-radius: 3px;
+    font-family: "SF Mono", "Fira Code", "Fira Mono", Menlo, Consolas, 
monospace;
+    font-size: 0.8125rem;
+    white-space: pre-wrap;
+    word-break: break-word;
+    color: var(--text);
+}
+
+.schema-sql-copy {
+    position: absolute;
+    top: 0.375rem;
+    right: 0.375rem;
+    background: var(--copy-bg, transparent);
+    border: 1px solid var(--border);
+    border-radius: 3px;
+    cursor: pointer;
+    font-size: 0.875rem;
+    line-height: 1;
+    padding: 0.25rem 0.375rem;
+    color: var(--text-secondary);
+    transition: background 0.15s, color 0.15s;
+}
+
+.schema-sql-copy:hover {
+    background: var(--copy-bg-hover, var(--surface));
+    color: var(--text);
+}
+
+.schema-section > .detail-section-body > .schema-sql {
+    margin: 0;
+    border-radius: 0;
+}
+
 /* Status Tab */
 .status-controls {
     display: flex;
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/rqlite-10.0.5/http/console/static/js/app.js 
new/rqlite-10.1.0/http/console/static/js/app.js
--- old/rqlite-10.0.5/http/console/static/js/app.js     2026-05-12 
18:52:08.000000000 +0200
+++ new/rqlite-10.1.0/http/console/static/js/app.js     2026-05-20 
08:23:17.000000000 +0200
@@ -26,6 +26,47 @@
         return div.innerHTML.replace(/"/g, "&quot;").replace(/'/g, "&#39;");
     }
 
+    function formatTimeOfDay(date) {
+        var d = date || new Date();
+        var hh = String(d.getHours()).padStart(2, "0");
+        var mm = String(d.getMinutes()).padStart(2, "0");
+        var ss = String(d.getSeconds()).padStart(2, "0");
+        return hh + ":" + mm + ":" + ss;
+    }
+
+    // copyToClipboard copies text to the clipboard, returning a Promise that
+    // resolves on success and rejects on failure. Uses the async Clipboard
+    // API when available (secure contexts only), otherwise falls back to a
+    // hidden textarea + document.execCommand("copy") for plain-HTTP serves.
+    function copyToClipboard(text) {
+        if (navigator.clipboard && window.isSecureContext) {
+            return navigator.clipboard.writeText(text);
+        }
+        return new Promise(function (resolve, reject) {
+            var ta = document.createElement("textarea");
+            ta.value = text;
+            ta.setAttribute("readonly", "");
+            ta.style.position = "fixed";
+            ta.style.top = "0";
+            ta.style.left = "0";
+            ta.style.opacity = "0";
+            document.body.appendChild(ta);
+            ta.select();
+            var ok = false;
+            try {
+                ok = document.execCommand("copy");
+            } catch (e) {
+                ok = false;
+            }
+            document.body.removeChild(ta);
+            if (ok) {
+                resolve();
+            } else {
+                reject(new Error("Copy not supported in this context"));
+            }
+        });
+    }
+
     // --- Dark Mode ---
 
     var THEME_KEY = "rqlite_theme";
@@ -88,6 +129,9 @@
             tabContents.forEach(function (tc) { tc.classList.remove("active"); 
});
             tab.classList.add("active");
             document.getElementById(target).classList.add("active");
+            if (target === "schema") {
+                loadSchema();
+            }
         });
     });
 
@@ -96,6 +140,30 @@
     var sqlInput = document.getElementById("sql-input");
     var executeBtn = document.getElementById("execute-btn");
     var resultsDiv = document.getElementById("query-results");
+    var queryLevel = document.getElementById("query-level");
+    var queryFreshness = document.getElementById("query-freshness");
+    var queryFreshnessStrict = 
document.getElementById("query-freshness-strict");
+    var queryActions = document.querySelector("#query .query-actions");
+
+    var QUERY_LEVEL_KEY = "rqlite_query_level";
+    var savedLevel = localStorage.getItem(QUERY_LEVEL_KEY);
+    if (savedLevel) {
+        queryLevel.value = savedLevel;
+    }
+    updateFreshnessVisibility();
+
+    queryLevel.addEventListener("change", function () {
+        localStorage.setItem(QUERY_LEVEL_KEY, queryLevel.value);
+        updateFreshnessVisibility();
+    });
+
+    function updateFreshnessVisibility() {
+        if (queryLevel.value === "none") {
+            queryActions.classList.add("show-freshness");
+        } else {
+            queryActions.classList.remove("show-freshness");
+        }
+    }
 
     executeBtn.addEventListener("click", executeQuery);
 
@@ -181,6 +249,24 @@
 
     renderHistory();
 
+    function buildRequestURL() {
+        var params = ["timings"];
+        var level = queryLevel.value;
+        if (level && level !== "weak") {
+            params.push("level=" + encodeURIComponent(level));
+        }
+        if (level === "none") {
+            var f = queryFreshness.value.trim();
+            if (f) {
+                params.push("freshness=" + encodeURIComponent(f));
+            }
+            if (queryFreshnessStrict.checked) {
+                params.push("freshness_strict");
+            }
+        }
+        return "/db/request?" + params.join("&");
+    }
+
     // --- Last query results (for export) ---
     var lastQueryResults = null;
 
@@ -193,7 +279,7 @@
 
         saveToHistory(sql);
 
-        apiRequest("POST", "/db/request?timings", [sql])
+        apiRequest("POST", buildRequestURL(), [sql])
             .then(function (resp) {
                 lastQueryResults = resp.data;
                 renderResults(resp.data);
@@ -312,10 +398,13 @@
             text = JSON.stringify(rows, null, 2);
         }
 
-        navigator.clipboard.writeText(text).then(function () {
-            var orig = btn.textContent;
+        var orig = btn.textContent;
+        copyToClipboard(text).then(function () {
             btn.textContent = "Copied!";
             setTimeout(function () { btn.textContent = orig; }, 1500);
+        }, function () {
+            btn.textContent = "Copy failed";
+            setTimeout(function () { btn.textContent = orig; }, 1500);
         });
     }
 
@@ -386,9 +475,13 @@
     });
 
     copyJsonBtn.addEventListener("click", function () {
-        navigator.clipboard.writeText(rawJsonPre.textContent).then(function () 
{
+        copyToClipboard(rawJsonPre.textContent).then(function () {
             copyJsonBtn.textContent = "\u2714";
             setTimeout(function () { copyJsonBtn.textContent = "\u2398"; }, 
1500);
+        }, function () {
+            copyJsonBtn.title = "Copy not supported in this context";
+            copyJsonBtn.textContent = "\u2718";
+            setTimeout(function () { copyJsonBtn.textContent = "\u2398"; }, 
1500);
         });
     });
 
@@ -412,12 +505,7 @@
             if (!rawJsonWrapper.classList.contains("hidden")) {
                 rawJsonPre.textContent = JSON.stringify(lastStatusData, null, 
2);
             }
-            // Update last-updated timestamp
-            var now = new Date();
-            var hh = String(now.getHours()).padStart(2, "0");
-            var mm = String(now.getMinutes()).padStart(2, "0");
-            var ss = String(now.getSeconds()).padStart(2, "0");
-            lastUpdatedSpan.textContent = "Updated " + hh + ":" + mm + ":" + 
ss;
+            lastUpdatedSpan.textContent = "Updated " + formatTimeOfDay();
         }).catch(function (err) {
             statusCards.innerHTML = '<div class="status-error">' + 
escapeHTML(err.message) + '</div>';
         });
@@ -464,6 +552,7 @@
         var oss = data.os || {};
         var rt = data.runtime || {};
         var cluster = data.cluster || {};
+        var mux = data.mux || {};
         var snapStore = store.snapshot_store || {};
 
         var sections = [
@@ -533,7 +622,7 @@
                 rows: [
                     ["Address", cluster.addr],
                     ["API Address", cluster.api_addr],
-                    ["HTTPS", cluster.https]
+                    ["TLS", mux.tls]
                 ]
             },
             {
@@ -753,4 +842,314 @@
                 backupBtn.disabled = false;
             });
     });
+
+    // --- Schema Tab ---
+
+    var schemaContent = document.getElementById("schema-content");
+    var schemaRefreshBtn = document.getElementById("schema-refresh-btn");
+    var schemaLastUpdated = document.getElementById("schema-last-updated");
+
+    var SCHEMA_TABLES_QUERY = "SELECT m.name AS table_name, m.sql AS 
table_sql, p.name AS column_name, p.type, p.\"notnull\" AS not_null, 
p.dflt_value, p.pk FROM sqlite_master m JOIN pragma_table_info(m.name) p WHERE 
m.type = 'table' ORDER BY m.name, p.cid";
+    var SCHEMA_OBJECTS_QUERY = "SELECT name, type, tbl_name, sql FROM 
sqlite_master WHERE type IN ('index', 'trigger') AND sql IS NOT NULL ORDER BY 
type, tbl_name, name";
+    var SCHEMA_COUNT_ROWS_KEY = "rqlite_schema_count_rows";
+
+    function shouldCountRows() {
+        return localStorage.getItem(SCHEMA_COUNT_ROWS_KEY) === "true";
+    }
+
+    schemaRefreshBtn.addEventListener("click", loadSchema);
+
+    function slugify(s) {
+        return String(s).toLowerCase().replace(/[^a-z0-9]+/g, 
"-").replace(/^-+|-+$/g, "");
+    }
+
+    function loadSchema() {
+        schemaRefreshBtn.disabled = true;
+        schemaContent.innerHTML = '<div class="schema-loading">Loading 
schema...</div>';
+
+        apiRequest("POST", "/db/query?associative", [SCHEMA_TABLES_QUERY, 
SCHEMA_OBJECTS_QUERY])
+            .then(function (resp) {
+                var tableNames = extractTableNames(resp.data);
+                if (tableNames.length === 0 || !shouldCountRows()) {
+                    renderSchema(resp.data, {});
+                    schemaLastUpdated.textContent = "Last updated: " + 
formatTimeOfDay();
+                    return;
+                }
+                var countQueries = tableNames.map(function (n) {
+                    return "SELECT COUNT(*) FROM " + sqlQuoteIdent(n);
+                });
+                return apiRequest("POST", "/db/query?level=none", countQueries)
+                    .then(function (cresp) {
+                        var counts = {};
+                        var results = (cresp.data && cresp.data.results) || [];
+                        results.forEach(function (r, i) {
+                            if (r && !r.error && r.values && r.values[0]) {
+                                counts[tableNames[i]] = r.values[0][0];
+                            }
+                        });
+                        renderSchema(resp.data, counts);
+                        schemaLastUpdated.textContent = "Last updated: " + 
formatTimeOfDay();
+                    }, function () {
+                        renderSchema(resp.data, {});
+                        schemaLastUpdated.textContent = "Last updated: " + 
formatTimeOfDay();
+                    });
+            })
+            .catch(function (err) {
+                schemaContent.innerHTML = '<div class="result-error">' + 
escapeHTML(err.message) + '</div>';
+            })
+            .finally(function () {
+                schemaRefreshBtn.disabled = false;
+            });
+    }
+
+    function extractTableNames(data) {
+        var names = [];
+        var seen = {};
+        var rows = (data && data.results && data.results[0] && 
data.results[0].rows) || [];
+        rows.forEach(function (row) {
+            var n = row.table_name;
+            if (n && !seen[n]) {
+                seen[n] = true;
+                names.push(n);
+            }
+        });
+        return names;
+    }
+
+    function renderSchema(data, rowCounts) {
+        rowCounts = rowCounts || {};
+        if (!data || !data.results || data.results.length === 0) {
+            schemaContent.innerHTML = '<div class="result-error">No results 
returned</div>';
+            return;
+        }
+        var tableResult = data.results[0] || {};
+        var objectResult = data.results[1] || {};
+        if (tableResult.error) {
+            schemaContent.innerHTML = '<div class="result-error">' + 
escapeHTML(tableResult.error) + '</div>';
+            return;
+        }
+
+        // Group table columns by table name, preserving order.
+        var tables = {};
+        var tableOrder = [];
+        (tableResult.rows || []).forEach(function (row) {
+            var name = row.table_name;
+            if (!tables[name]) {
+                tables[name] = { sql: row.table_sql, columns: [] };
+                tableOrder.push(name);
+            }
+            tables[name].columns.push(row);
+        });
+
+        // Group indexes and triggers by type.
+        var indexes = [];
+        var triggers = [];
+        (objectResult.rows || []).forEach(function (row) {
+            if (row.type === "index") indexes.push(row);
+            else if (row.type === "trigger") triggers.push(row);
+        });
+
+        if (tableOrder.length === 0 && indexes.length === 0 && triggers.length 
=== 0) {
+            schemaContent.innerHTML = '<div class="schema-empty">No tables, 
indexes, or triggers found.</div>';
+            return;
+        }
+
+        var html = "";
+
+        html += '<div class="schema-actions">';
+        html += '<button type="button" class="schema-expand-all">Expand 
all</button>';
+        html += '<button type="button" class="schema-collapse-all">Collapse 
all</button>';
+        html += '<label class="schema-count-toggle" title="Run COUNT(*) on 
every table when loading the schema">';
+        html += '<input type="checkbox" class="schema-count-rows"' + 
(shouldCountRows() ? ' checked' : '') + '> Count rows';
+        html += '</label>';
+        html += '</div>';
+
+        // Tables.
+        tableOrder.forEach(function (tableName) {
+            var t = tables[tableName];
+            var anchor = "schema-table-" + slugify(tableName);
+            html += '<div class="detail-section schema-section open" id="' + 
escapeHTML(anchor) + '">';
+            html += '<div class="detail-section-header">';
+            html += '<span><span class="schema-kind">table</span> ' + 
escapeHTML(tableName);
+            var columnsStr = t.columns.length + ' column' + (t.columns.length 
=== 1 ? '' : 's');
+            html += ' <span class="schema-count">(' + escapeHTML(columnsStr);
+            if (Object.prototype.hasOwnProperty.call(rowCounts, tableName)) {
+                var rc = Number(rowCounts[tableName]);
+                var rowsStr = rc.toLocaleString() + ' row' + (rc === 1 ? '' : 
's');
+                html += ', <span class="schema-rowcount-text" title="Row count 
read with &#39;none&#39; consistency &mdash; may be slightly stale">' + 
escapeHTML(rowsStr) + '</span>';
+            }
+            html += ')</span></span>';
+            html += '<span class="arrow">&#9654;</span>';
+            html += '</div>';
+            html += '<div class="detail-section-body">';
+            html += '<table class="result-table schema-columns"><thead><tr>';
+            html += '<th>Column</th>';
+            html += '<th>Type</th>';
+            html += '<th class="schema-center">Not Null</th>';
+            html += '<th>Default</th>';
+            html += '</tr></thead><tbody>';
+            t.columns.forEach(function (col) {
+                html += '<tr' + (col.pk ? ' class="schema-pk-row"' : '') + '>';
+                html += '<td>' + escapeHTML(col.column_name);
+                if (col.pk) {
+                    html += ' <span class="schema-pk-badge" title="Primary 
key">&#128273;</span>';
+                }
+                html += '</td>';
+                html += '<td>' + escapeHTML(col.type) + '</td>';
+                html += '<td class="schema-center">' + (col.not_null
+                    ? '<span class="schema-check" title="Value may not be 
NULL">&#10003;</span>'
+                    : '<span class="schema-dash" title="Value may be 
NULL">&mdash;</span>') + '</td>';
+                if (col.dflt_value === null || col.dflt_value === undefined) {
+                    html += '<td class="null-value">NULL</td>';
+                } else {
+                    html += '<td>' + escapeHTML(col.dflt_value) + '</td>';
+                }
+                html += '</tr>';
+            });
+            html += '</tbody></table>';
+            html += '<div class="schema-sql-block">';
+            html += '<div class="schema-sql-actions">';
+            if (t.sql) {
+                html += '<button class="schema-sql-toggle" type="button">Show 
CREATE TABLE</button>';
+            } else {
+                html += '<span></span>';
+            }
+            html += '<button class="schema-drop-table" type="button" 
data-table-name="' + escapeHTML(tableName) + '">Drop table</button>';
+            html += '</div>';
+            if (t.sql) {
+                html += '<div class="schema-sql-wrapper hidden">';
+                html += '<button type="button" class="schema-sql-copy" 
title="Copy to clipboard">&#x2398;</button>';
+                html += '<pre class="schema-sql">' + escapeHTML(t.sql) + 
'</pre>';
+                html += '</div>';
+            }
+            html += '</div>';
+            html += '</div></div>';
+        });
+
+        // Indexes.
+        indexes.forEach(function (idx) {
+            var anchor = "schema-index-" + slugify(idx.name);
+            html += '<div class="detail-section schema-section" id="' + 
escapeHTML(anchor) + '">';
+            html += '<div class="detail-section-header">';
+            html += '<span><span class="schema-kind">index</span> ' + 
escapeHTML(idx.name);
+            html += ' <span class="schema-count">on ' + 
escapeHTML(idx.tbl_name) + '</span></span>';
+            html += '<span class="arrow">&#9654;</span>';
+            html += '</div>';
+            html += '<div class="detail-section-body">';
+            html += '<pre class="schema-sql">' + escapeHTML(idx.sql) + 
'</pre>';
+            html += '</div></div>';
+        });
+
+        // Triggers.
+        triggers.forEach(function (trg) {
+            var anchor = "schema-trigger-" + slugify(trg.name);
+            html += '<div class="detail-section schema-section" id="' + 
escapeHTML(anchor) + '">';
+            html += '<div class="detail-section-header">';
+            html += '<span><span class="schema-kind">trigger</span> ' + 
escapeHTML(trg.name);
+            html += ' <span class="schema-count">on ' + 
escapeHTML(trg.tbl_name) + '</span></span>';
+            html += '<span class="arrow">&#9654;</span>';
+            html += '</div>';
+            html += '<div class="detail-section-body">';
+            html += '<pre class="schema-sql">' + escapeHTML(trg.sql) + 
'</pre>';
+            html += '</div></div>';
+        });
+
+        schemaContent.innerHTML = html;
+    }
+
+    function sqlQuoteIdent(name) {
+        return '"' + String(name).replace(/"/g, '""') + '"';
+    }
+
+    function dropTable(tableName, btn) {
+        btn.disabled = true;
+        var sql = "DROP TABLE " + sqlQuoteIdent(tableName);
+        apiRequest("POST", "/db/execute", [sql])
+            .then(function (resp) {
+                var results = (resp.data && resp.data.results) || [];
+                var err = results[0] && results[0].error;
+                if (err) {
+                    window.alert("Failed to drop table \"" + tableName + "\": 
" + err);
+                    btn.disabled = false;
+                    return;
+                }
+                loadSchema();
+            })
+            .catch(function (err) {
+                window.alert("Failed to drop table \"" + tableName + "\": " + 
err.message);
+                btn.disabled = false;
+            });
+    }
+
+    schemaContent.addEventListener("change", function (e) {
+        var t = e.target;
+        if (t && t.classList && t.classList.contains("schema-count-rows")) {
+            localStorage.setItem(SCHEMA_COUNT_ROWS_KEY, t.checked ? "true" : 
"false");
+            loadSchema();
+        }
+    });
+
+    schemaContent.addEventListener("click", function (e) {
+        var header = e.target.closest && 
e.target.closest(".detail-section-header");
+        if (header && schemaContent.contains(header)) {
+            header.parentElement.classList.toggle("open");
+            return;
+        }
+
+        var btn = e.target;
+        if (!btn.classList) return;
+
+        if (btn.classList.contains("schema-sql-toggle")) {
+            var block = btn.closest(".schema-sql-block");
+            var wrapper = block ? block.querySelector(".schema-sql-wrapper") : 
null;
+            if (!wrapper) return;
+            if (wrapper.classList.contains("hidden")) {
+                wrapper.classList.remove("hidden");
+                btn.textContent = "Hide CREATE TABLE";
+            } else {
+                wrapper.classList.add("hidden");
+                btn.textContent = "Show CREATE TABLE";
+            }
+            return;
+        }
+
+        if (btn.classList.contains("schema-sql-copy")) {
+            var wrap = btn.closest(".schema-sql-wrapper");
+            var preEl = wrap ? wrap.querySelector(".schema-sql") : null;
+            if (!preEl) return;
+            copyToClipboard(preEl.textContent).then(function () {
+                btn.textContent = "✔";
+                setTimeout(function () { btn.innerHTML = "⎘"; }, 1500);
+            }, function () {
+                btn.title = "Copy not supported in this context";
+                btn.textContent = "✘";
+                setTimeout(function () { btn.innerHTML = "⎘"; }, 1500);
+            });
+            return;
+        }
+
+        if (btn.classList.contains("schema-drop-table")) {
+            var tableName = btn.getAttribute("data-table-name");
+            if (!tableName) return;
+            var ok = window.confirm("Drop table \"" + tableName + "\"?\n\nThis 
permanently deletes the table and all its data. This action cannot be undone.");
+            if (!ok) return;
+            dropTable(tableName, btn);
+            return;
+        }
+
+
+        if (btn.classList.contains("schema-expand-all")) {
+            schemaContent.querySelectorAll(".schema-section").forEach(function 
(s) {
+                s.classList.add("open");
+            });
+            return;
+        }
+
+        if (btn.classList.contains("schema-collapse-all")) {
+            schemaContent.querySelectorAll(".schema-section").forEach(function 
(s) {
+                s.classList.remove("open");
+            });
+            return;
+        }
+    });
 })();

++++++ vendor.tar.xz ++++++

Reply via email to