Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
3a3c0aa
Base
DanielCardonaRojas May 4, 2026
efffb80
Fix syntax highlight
DanielCardonaRojas May 4, 2026
a4228b1
Fix tab bar collapse state
DanielCardonaRojas May 4, 2026
eb89b7e
Fix filters
DanielCardonaRojas May 5, 2026
28146b6
Show all tours in my tours
DanielCardonaRojas May 5, 2026
bd27af8
Query syntax highlighting
DanielCardonaRojas May 5, 2026
5136680
Add user icon beside author in browser
DanielCardonaRojas May 5, 2026
87134ea
Update docs
DanielCardonaRojas May 6, 2026
d8e3c88
Support tags in tours
DanielCardonaRojas May 6, 2026
625b27c
Tour details styling
DanielCardonaRojas May 6, 2026
a44e9a6
Add links feature
DanielCardonaRojas May 6, 2026
fea9b6e
Colored tags
DanielCardonaRojas May 6, 2026
3f52e65
Update cards
DanielCardonaRojas May 6, 2026
66677a0
Lint and format
DanielCardonaRojas May 6, 2026
45c738e
Code review
DanielCardonaRojas May 6, 2026
8f5865a
Address code review comments from PR #44
DanielCardonaRojas May 6, 2026
534d328
Add preferences
DanielCardonaRojas May 6, 2026
162b989
Lint and format
DanielCardonaRojas May 6, 2026
ee03b34
Code review
DanielCardonaRojas May 6, 2026
02a7dbc
Highlights
DanielCardonaRojas May 7, 2026
8d34681
Merge main into web_ui
DanielCardonaRojas May 7, 2026
b85fd54
Sticky lines
DanielCardonaRojas May 7, 2026
16b4314
Swift syntax highlighting
DanielCardonaRojas May 7, 2026
578af2d
Sticky lines
DanielCardonaRojas May 7, 2026
cff45f6
Merge branch 'main' into web_ui
DanielCardonaRojas May 7, 2026
69bccd3
Merge branch 'main' into web_ui
DanielCardonaRojas May 9, 2026
2bedd18
Merge branch 'main' into web_ui
DanielCardonaRojas May 9, 2026
0cb6c5a
Operate in registry mode
DanielCardonaRojas May 9, 2026
0e9c43e
Add registry mode flag
DanielCardonaRojas May 9, 2026
8da9008
Update browser repo dropdown
DanielCardonaRojas May 9, 2026
846d7e2
Updates
DanielCardonaRojas May 10, 2026
59ebd51
Fix filtering
DanielCardonaRojas May 10, 2026
03751f9
Update layout
DanielCardonaRojas May 10, 2026
d7097aa
Fetch notes for tour details
DanielCardonaRojas May 10, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
463 changes: 458 additions & 5 deletions Cargo.lock

Large diffs are not rendered by default.

5 changes: 4 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ resolver = "2"
axum = "0.7"
tokio = { version = "1", features = ["full"] }
tower = "0.5"
tower-http = { version = "0.5", features = ["trace"] }
tower-http = { version = "0.5", features = ["trace", "set-header"] }
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter", "json"] }
serde = { version = "1", features = ["derive"] }
Expand All @@ -27,3 +27,6 @@ rusqlite = { version = "0.37", features = ["bundled"] }
tree-sitter = "0.25"
tempfile = "3"
deadpool-sqlite = "0.12"
rinja = "0.3"
rinja_axum = "0.3"
syntect = { version = "5", default-features = false, features = ["parsing", "html", "regex-fancy", "default-syntaxes", "plist-load"] }
7 changes: 7 additions & 0 deletions codetours.toml.example
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ max_open_tenants = 256
# Maximum allowed size for a SQLite pack upload in bytes. Default: 5MB
max_pack_size = 5242880

[ui]
# Optional path to a custom CSS file to override Tailwind @theme variables.
# theme_css = "/path/to/theme.css"

[storage]
# Number of SQLite connections in the pool per tenant. Default: 8
pool_size = 8
Expand All @@ -30,6 +34,9 @@ mode = "stub"
# Clients must provide this in the 'X-Tour-Token' header.
dev_token = "dev-token"

# Used in Phase 5 for "My Tours" local-dev mock before real auth lands.
stub_user = "local-dev"


# ------------------------------------------------------------------------------
# Codemark CLI Client Configuration (~/.codemark/config.toml)
Expand Down
1 change: 1 addition & 0 deletions crates/codemark-cli/src/cli/handlers/pull.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ pub async fn handle_pull(cli: &Cli, mode: &OutputMode, args: &PullArgs) -> Resul
let client = build_pull_http_client()?;
let response = client
.get(format!("{}/tours", server_url))
.header(reqwest::header::ACCEPT, "application/json")
.send()
.await
.map_err(|e| Error::Operation(format!("failed to query tours list: {e}")))?;
Expand Down
1 change: 1 addition & 0 deletions crates/codemark-cli/src/cli/handlers/tour.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ pub async fn handle_tour_list(cli: &Cli, _mode: &OutputMode, args: &TourListArgs
// 2. Query server
let client = reqwest::Client::new();
let mut headers = HeaderMap::new();
headers.insert(reqwest::header::ACCEPT, HeaderValue::from_static("application/json"));
if let Some(t) = &token {
headers.insert(
"X-Tour-Token",
Expand Down
11 changes: 11 additions & 0 deletions crates/codetours-server/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,19 @@ uuid = { workspace = true }
chrono = { workspace = true }
rusqlite = { workspace = true }
deadpool-sqlite = { workspace = true }
rinja = { workspace = true }
rinja_axum = { workspace = true }
syntect = { workspace = true }
syntect-assets = "0.23"
futures-util = "0.3"
zstd = "0.13"
quick_cache = "0.6.21"
xxhash-rust = { version = "0.8.15", features = ["xxh3"] }

[dev-dependencies]
criterion = "0.8.2"
tempfile = { workspace = true }

[[bench]]
name = "highlight"
harness = false
34 changes: 34 additions & 0 deletions crates/codetours-server/benches/highlight.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
use codetours_server::highlight::{get_cache, highlight};
use criterion::{Criterion, criterion_group, criterion_main};
use std::hint::black_box;

fn bench_highlight(c: &mut Criterion) {
let language = "rust";
let content = r#"
pub fn fibonacci(n: u32) -> u32 {
match n {
0 => 1,
1 => 1,
_ => fibonacci(n - 1) + fibonacci(n - 2),
}
}
"#;

// Warm up the cache
let _ = highlight(language, content);

c.bench_function("highlight_cached", |b| {
b.iter(|| highlight(black_box(language), black_box(content)));
});

c.bench_function("highlight_uncached", |b| {
b.iter(|| {
// clear the cache each iteration to simulate a miss
get_cache().clear();
highlight(black_box(language), black_box(content))
});
});
}

criterion_group!(benches, bench_highlight);
criterion_main!(benches);
48 changes: 48 additions & 0 deletions crates/codetours-server/build.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
use std::env;
use std::process::Command;

fn main() {
println!("cargo:rerun-if-changed=static/app.css");
println!("cargo:rerun-if-changed=templates/");

let out_dir = env::var("CARGO_MANIFEST_DIR").unwrap();
let input = format!("{}/static/app.css", out_dir);
let output = format!("{}/static/app.generated.css", out_dir);

let skip_tailwind = env::var("SKIP_TAILWIND_BUILD").unwrap_or_default() == "1";
if skip_tailwind {
println!("cargo:warning=SKIP_TAILWIND_BUILD is set, skipping regeneration.");
return;
}

let status = Command::new("tailwindcss")
.args(["-i", &input, "-o", &output, "--minify"])
.status()
.or_else(|_| {
Command::new("../../tailwindcss")
.args(["-i", &input, "-o", &output, "--minify"])
.status()
});

match status {
Ok(status) if status.success() => (),
Ok(status) => {
eprintln!("Fatal: tailwindcss exited with status {status}.");
std::process::exit(1);
}
Err(err) => {
eprintln!("Error: tailwindcss CLI not found or failed.");
eprintln!("Details: {err}");
eprintln!("Please install it via 'cargo binstall tailwindcss' or 'mise install'.");
// If the generated file already exists (e.g., committed in CI), allow the
// build to proceed with the stale artifact rather than blocking CI entirely.
let output_path = std::path::Path::new(&output);
if !output_path.exists() {
eprintln!("Fatal: {} does not exist. Cannot continue.", output);
std::process::exit(1);
} else {
eprintln!("Warning: using existing {} — CSS may be stale.", output);
}
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}
21 changes: 21 additions & 0 deletions crates/codetours-server/examples/check_syntax.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
use syntect_assets::assets::HighlightingAssets;

fn main() {
let assets = HighlightingAssets::from_binary();
let ss = assets.get_syntax_set().unwrap();
println!("Total syntaxes: {}", ss.syntaxes().len());

// Check for Swift
let swift_syntax = ss.find_syntax_by_name("Swift");
match swift_syntax {
Some(s) => println!("Found Swift: {} - extensions: {:?}", s.name, s.file_extensions),
None => println!("Swift NOT found in syntax set"),
}

// Check if we can find it by extension
let by_ext = ss.find_syntax_by_extension("swift");
match by_ext {
Some(s) => println!("Found by extension 'swift': {}", s.name),
None => println!("NOT found by extension 'swift'"),
}
}
2 changes: 2 additions & 0 deletions crates/codetours-server/rinja.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
[filter]
tag_color_classes = "crate::web::filters::tag_color_classes"
12 changes: 11 additions & 1 deletion crates/codetours-server/src/auth/extractor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,16 @@ impl AuthContext {
matches!(self, AuthContext::Authenticated { .. })
}

/// Returns the active user_id for queries that need to key off "me".
/// PHASE 6 SWAP POINT: In M2, this falls back to the configured `stub_user` for anonymous callers.
/// In Phase 6, anonymous callers will return None, and only authenticated callers will return Some(user_id).
pub fn current_user(&self, config: &crate::config::Config) -> Option<String> {
match self {
AuthContext::Authenticated { user_id, .. } => Some(user_id.clone()),
AuthContext::Anonymous => Some(config.auth.stub_user.clone()),
}
}

/// Returns true if the authenticated user has the specified scope.
pub fn has_scope(&self, scope: Scope) -> bool {
match self {
Expand Down Expand Up @@ -120,7 +130,7 @@ where
}

Ok(AuthContext::Authenticated {
user_id: "stub".to_string(),
user_id: state.config.auth.stub_user.clone(),
scopes: vec![Scope::Publish, Scope::Read, Scope::Delete],
})
}
Expand Down
12 changes: 12 additions & 0 deletions crates/codetours-server/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,18 @@ pub struct Cli {
#[arg(long, help = "Enable JSON structured logging")]
pub json_logs: bool,

#[arg(
long,
help = "Enable registry mode: aggregate tours from all repositories in the global registry"
)]
pub registry_mode: bool,

#[arg(
long,
help = "Path to the registry database (only used with --registry-mode)"
)]
pub registry_path: Option<PathBuf>,

#[command(subcommand)]
pub command: Option<Command>,
}
Expand Down
24 changes: 22 additions & 2 deletions crates/codetours-server/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,26 @@ pub struct Config {
pub storage: StorageConfig,
#[serde(default)]
pub auth: AuthConfig,
#[serde(default)]
pub ui: UiConfig,
}

#[derive(Deserialize, Debug, Clone, Default)]
#[serde(deny_unknown_fields)]
pub struct UiConfig {
pub theme_css: Option<String>,
}

#[derive(Deserialize, Debug, Clone)]
#[serde(deny_unknown_fields)]
pub struct StorageConfig {
#[serde(default = "default_pool_size")]
pub pool_size: u32,
/// Enable registry mode for aggregating data from multiple repositories
#[serde(default)]
pub registry_mode: bool,
/// Path to the registry database (defaults to global config dir)
pub registry_path: Option<PathBuf>,
}

#[derive(Deserialize, Debug, Clone)]
Expand All @@ -36,6 +49,8 @@ pub struct AuthConfig {
pub mode: String,
#[serde(default)]
pub dev_token: String,
#[serde(default = "default_stub_user")]
pub stub_user: String,
}

fn default_host() -> String {
Expand Down Expand Up @@ -70,6 +85,10 @@ fn default_auth_mode() -> String {
"stub".to_string()
}

fn default_stub_user() -> String {
"local-dev".to_string()
}

impl Default for Config {
fn default() -> Self {
Self {
Expand All @@ -81,19 +100,20 @@ impl Default for Config {
max_pack_size: default_max_pack_size(),
storage: StorageConfig::default(),
auth: AuthConfig::default(),
ui: UiConfig::default(),
}
}
}

impl Default for StorageConfig {
fn default() -> Self {
Self { pool_size: default_pool_size() }
Self { pool_size: default_pool_size(), registry_mode: false, registry_path: None }
}
}

impl Default for AuthConfig {
fn default() -> Self {
Self { mode: default_auth_mode(), dev_token: String::new() }
Self { mode: default_auth_mode(), dev_token: String::new(), stub_user: default_stub_user() }
}
}

Expand Down
92 changes: 92 additions & 0 deletions crates/codetours-server/src/handlers/config.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
use crate::web::NavItem;
use crate::{auth::AuthContext, router::AppState};
use axum::{
extract::{Form, State},
http::StatusCode,
response::IntoResponse,
};
use rinja::Template;
use serde::Deserialize;

#[derive(Template)]
#[template(path = "config/index.html")]
pub struct ConfigTemplate {
pub nav: NavItem,
pub repos: Vec<RepoView>,
pub prefs: PrefsView,
}

pub struct PrefsView {
pub theme: String,
pub font: String,
}

pub struct RepoView {
pub name: String,
pub path: String,
pub connected: bool,
}

#[derive(Deserialize)]
pub struct PrefsForm {
pub theme: String,
pub font: String,
}

pub async fn page_handler(State(state): State<AppState>, _auth: AuthContext) -> impl IntoResponse {
let db = match state.storage.get_conn().await {
Ok(t) => t,
Err(_) => return StatusCode::INTERNAL_SERVER_ERROR.into_response(),
};

let repos = db
.interact(|conn| {
let mut stmt = conn
.prepare(
"
SELECT DISTINCT repo_url
FROM collections
WHERE repo_url IS NOT NULL AND repo_url != ''
",
)
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;

let urls = stmt
.query_map([], |row| row.get::<_, String>(0))
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?
.collect::<Result<Vec<_>, _>>()
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;

let mut repos = Vec::new();
for url in urls {
let name = url.split('/').next_back().unwrap_or(&url).to_string();
repos.push(RepoView { name, path: url, connected: true });
Comment on lines +62 to +63

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Handle trailing slashes when deriving repository names.

If repo_url ends with /, Line 62 can produce an empty name, which renders poorly in the config UI. Trim trailing / before splitting (or filter empty segments) to keep labels stable.

Suggested fix
-                let name = url.split('/').next_back().unwrap_or(&url).to_string();
+                let normalized = url.trim_end_matches('/');
+                let name = normalized
+                    .rsplit('/')
+                    .find(|segment| !segment.is_empty())
+                    .unwrap_or(normalized)
+                    .to_string();
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/codetours-server/src/handlers/config.rs` around lines 62 - 63, When
deriving the RepoView.name from the repo URL, avoid producing an empty string
for URLs with trailing slashes by trimming trailing '/' characters (or filtering
out empty segments) before splitting; update the code that builds RepoView
(where `let name = url.split('/').next_back().unwrap_or(&url).to_string();
repos.push(RepoView { name, path: url, connected: true });`) to first
canonicalize the URL string (e.g., remove trailing slashes or use
`url.rsplit('/').find(|s| !s.is_empty())`) and then use that non-empty segment
as `name`.

}
Ok::<_, StatusCode>(repos)
})
.await
.unwrap_or(Err(StatusCode::INTERNAL_SERVER_ERROR));

match repos {
Ok(repos) => {
let prefs =
PrefsView { theme: "atom-one-dark".to_string(), font: "Fira Code".to_string() };
ConfigTemplate { nav: NavItem::Config, repos, prefs }.into_response()
}
Err(e) => e.into_response(),
}
}

pub async fn prefs_handler(
State(_state): State<AppState>,
_auth: AuthContext,
Form(_form): Form<PrefsForm>,
) -> impl IntoResponse {
// TODO: Persist preferences in Phase 10
StatusCode::NOT_IMPLEMENTED.into_response()
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

pub async fn stub_handler() -> impl IntoResponse {
(StatusCode::NOT_IMPLEMENTED, "This feature will be available in Phase 10 or later.")
.into_response()
}
5 changes: 5 additions & 0 deletions crates/codetours-server/src/handlers/home.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
use axum::response::{IntoResponse, Redirect};

pub async fn handler() -> impl IntoResponse {
Redirect::temporary("/tours")
}
Loading
Loading