Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
10 changes: 10 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -51,3 +51,13 @@ shock.code-workspace
devel/lo.data
shock-server/shock-server
shock-server/shock-server

# Node.js
node_modules/

# UI build outputs (embedded copy)
shock-server/ui/dist/
!shock-server/ui/dist/.gitkeep
clients/shock-ts/dist/
clients/shock-ui/dist/
*.tsbuildinfo
10 changes: 10 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,13 @@
# This allows the shock-server to bind to port 80 if desired.
#setcap 'cap_net_bind_service=+ep' bin/shock-server

FROM node:20-alpine AS ui-builder
WORKDIR /build/clients
COPY clients/ .
RUN npm install && \
cd shock-ts && npm run build && \
cd ../shock-ui && npm run build

FROM golang:alpine

ENV PYTHONUNBUFFERED=1
Expand All @@ -19,6 +26,9 @@ WORKDIR /go/bin

COPY . /go/src/github.com/MG-RAST/Shock

# Copy built UI into the Go embed directory
COPY --from=ui-builder /build/clients/shock-ui/dist ${DIR}/shock-server/ui/dist

RUN mkdir -p /var/log/shock /usr/local/shock/data ${DIR}

# set version
Expand Down
25 changes: 25 additions & 0 deletions build-ui.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
#!/bin/sh
set -e

SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
CLIENTS_DIR="$SCRIPT_DIR/clients"
UI_DIR="$CLIENTS_DIR/shock-ui"
EMBED_DIR="$SCRIPT_DIR/shock-server/ui/dist"

echo "==> Installing dependencies..."
cd "$CLIENTS_DIR"
npm install

echo "==> Building shock-ts..."
cd "$CLIENTS_DIR/shock-ts"
npm run build

echo "==> Building shock-ui..."
cd "$UI_DIR"
npm run build

echo "==> Copying dist to shock-server/ui/dist..."
rm -rf "$EMBED_DIR"
cp -r "$UI_DIR/dist" "$EMBED_DIR"

echo "==> UI build complete. Run ./compile-server.sh to embed in binary."
7 changes: 7 additions & 0 deletions clients/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"private": true,
"workspaces": [
"shock-ts",
"shock-ui"
]
}
100 changes: 98 additions & 2 deletions clients/shock-ts/src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,12 @@ import type {
DisplayAcl,
AclType,
DownloadOptions,
LocationInfo,
LocationNodeList,
LockedFiles,
LockedIndexes,
LockedNodes,
LockerState,
NodeListQuery,
NodeLocation,
PaginatedResult,
Expand All @@ -23,12 +29,14 @@ export class ShockClient {
private baseUrl: string;
private staticToken?: string;
private getTokenFn?: () => string | undefined;
private authType: "basic" | "oauth";

constructor(options: ShockClientOptions) {
// Strip trailing slash
this.baseUrl = options.url.replace(/\/+$/, "");
this.staticToken = options.token;
this.getTokenFn = options.getToken;
this.authType = options.authType ?? "oauth";
}

/** Update the static auth token. */
Expand Down Expand Up @@ -207,6 +215,92 @@ export class ShockClient {
return this.request<NodeLocation[]>("GET", `/node/${nodeId}/locations/`);
}

// ─── Index Management ────────────────────────────────────────

/** `DELETE /node/{id}/index/{type}` — delete an index. */
async deleteIndex(nodeId: string, indexType: string): Promise<void> {
this.validateId(nodeId);
await this.request<unknown>("DELETE", `/node/${nodeId}/index/${indexType}`);
}

// ─── Admin: Location Info ────────────────────────────────────

/** `GET /location/{loc}/info` — get location configuration info (admin only). */
async getLocationInfo(locId: string): Promise<LocationInfo> {
return this.request<LocationInfo>("GET", `/location/${locId}/info`);
}

/** `GET /location/{loc}/missing` — get nodes missing from a location (admin only). */
async getLocationMissing(locId: string): Promise<LocationNodeList> {
return this.request<LocationNodeList>("GET", `/location/${locId}/missing`);
}

/** `GET /location/{loc}/present` — get nodes present in a location (admin only). */
async getLocationPresent(locId: string): Promise<LocationNodeList> {
return this.request<LocationNodeList>("GET", `/location/${locId}/present`);
}

// ─── Admin: Locker ───────────────────────────────────────────

/** `GET /locker` — get all lock manager states (admin check). */
async getLocker(): Promise<LockerState> {
return this.request<LockerState>("GET", "/locker");
}

/** `GET /locked/node` — get locked node IDs. */
async getLockedNodes(): Promise<LockedNodes> {
return this.request<LockedNodes>("GET", "/locked/node");
}

/** `GET /locked/file` — get locked file IDs. */
async getLockedFiles(): Promise<LockedFiles> {
return this.request<LockedFiles>("GET", "/locked/file");
}

/** `GET /locked/index` — get locked index IDs. */
async getLockedIndexes(): Promise<LockedIndexes> {
return this.request<LockedIndexes>("GET", "/locked/index");
}

// ─── Admin: Trace ────────────────────────────────────────────

/** `GET /trace/start` — start execution trace (admin only). */
async startTrace(): Promise<string> {
return this.request<string>("GET", "/trace/start");
}

/** `GET /trace/stop` — stop execution trace (admin only). */
async stopTrace(): Promise<string> {
return this.request<string>("GET", "/trace/stop");
}

/** `GET /trace/download` — download the latest trace file as a Blob (admin only). */
async downloadTrace(): Promise<Blob> {
const response = await this.rawFetch("GET", "/trace/download");
if (!response.ok) {
await this.throwFromResponse(response);
}
return response.blob();
}

/** `GET /trace/summary` — get trace footprint summary as text (admin only). */
async getTraceSummary(): Promise<string> {
const response = await this.rawFetch("GET", "/trace/summary");
if (!response.ok) {
await this.throwFromResponse(response);
}
return response.text();
}

/** `GET /trace/events` — get parsed trace events as text (admin only). */
async getTraceEvents(): Promise<string> {
const response = await this.rawFetch("GET", "/trace/events");
if (!response.ok) {
await this.throwFromResponse(response);
}
return response.text();
}

// ─── Polling ───────────────────────────────────────────────────

/**
Expand Down Expand Up @@ -245,7 +339,8 @@ export class ShockClient {
private authHeaders(): Record<string, string> {
const token = this.resolveToken();
if (token) {
return { Authorization: `OAuth ${token}` };
const prefix = this.authType === "basic" ? "Basic" : "OAuth";
return { Authorization: `${prefix} ${token}` };
}
return {};
}
Expand Down Expand Up @@ -422,7 +517,8 @@ export class ShockClient {

const token = this.resolveToken();
if (token) {
xhr.setRequestHeader("Authorization", `OAuth ${token}`);
const prefix = this.authType === "basic" ? "Basic" : "OAuth";
xhr.setRequestHeader("Authorization", `${prefix} ${token}`);
}

xhr.upload.addEventListener("progress", (e) => {
Expand Down
6 changes: 6 additions & 0 deletions clients/shock-ts/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,4 +30,10 @@ export type {
ChunkedUploadOptions,
ChunkedUploadResult,
SmartUploadOptions,
LockerState,
LockedNodes,
LockedFiles,
LockedIndexes,
LocationInfo,
LocationNodeList,
} from "./types.js";
Loading