summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorMika Havela <mika.havela@gmail.com>2008-04-15 12:52:00 +0000
committerMika Havela <mika.havela@gmail.com>2008-04-15 12:52:00 +0000
commit612b0e754176f1b7098c5c1d7973028a22ee902d (patch)
tree79e410f870556474f297aa960251d428204d75b5
downloadacf-gnats-612b0e754176f1b7098c5c1d7973028a22ee902d.tar.bz2
acf-gnats-612b0e754176f1b7098c5c1d7973028a22ee902d.tar.xz
Creating a acf-gnats so that users can view/add bugreports to bugs@alpinelinux.org
git-svn-id: svn://svn.alpinelinux.org/acf/gnats/trunk@985 ab2d0c66-481e-0410-8bed-d214d4d58bed
-rw-r--r--Makefile54
-rw-r--r--README0
-rw-r--r--config.mk10
-rw-r--r--gnats-controller.lua515
-rw-r--r--gnats-edit-html.lsp58
-rw-r--r--gnats-expert-html.lsp52
-rw-r--r--gnats-model.lua498
-rw-r--r--gnats-query-html.lsp68
-rw-r--r--gnats-queryresult-html.lsp120
-rw-r--r--gnats-report-html.lsp77
-rw-r--r--gnats-reportsuccess-html.lsp2
-rw-r--r--gnats-status-html.lsp29
-rw-r--r--gnats-summary-html.lsp81
-rw-r--r--gnats.menu8
14 files changed, 1572 insertions, 0 deletions
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..6a0959d
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,54 @@
+APP_NAME=gnats
+PACKAGE=acf-$(APP_NAME)
+VERSION=0.1.0
+
+APP_DIST=\
+ gnats-controller.lua \
+ gnats-edit-html.lsp \
+ gnats-expert-html.lsp \
+ gnats-model.lua \
+ gnats-query-html.lsp \
+ gnats-queryresult-html.lsp \
+ gnats-report-html.lsp \
+ gnats-reportsuccess-html.lsp \
+ gnats-status-html.lsp \
+ gnats-summary-html.lsp \
+ gnats.menu \
+
+
+EXTRA_DIST=README Makefile config.mk
+
+DISTFILES=$(APP_DIST) $(EXTRA_DIST)
+
+TAR=tar
+
+P=$(PACKAGE)-$(VERSION)
+tarball=$(P).tar.bz2
+install_dir=$(DESTDIR)/$(appdir)/$(APP_NAME)
+
+all:
+clean:
+ rm -rf $(tarball) $(P)
+
+dist: $(tarball)
+
+install:
+ mkdir -p "$(install_dir)"
+ cp -a $(APP_DIST) "$(install_dir)"
+
+$(tarball): $(DISTFILES)
+ rm -rf $(P)
+ mkdir -p $(P)
+ cp $(DISTFILES) $(P)
+ $(TAR) -jcf $@ $(P)
+ rm -rf $(P)
+
+# target that creates a tar package, unpacks is and install from package
+dist-install: $(tarball)
+ $(TAR) -jxf $(tarball)
+ $(MAKE) -C $(P) install DESTDIR=$(DESTDIR)
+ rm -rf $(P)
+
+include config.mk
+
+.PHONY: all clean dist install dist-install
diff --git a/README b/README
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/README
diff --git a/config.mk b/config.mk
new file mode 100644
index 0000000..45f4d21
--- /dev/null
+++ b/config.mk
@@ -0,0 +1,10 @@
+prefix=/usr
+datadir=${prefix}/share
+sysconfdir=${prefix}/etc
+localstatedir=${prefix}/var
+acfdir=${datadir}/acf
+wwwdir=${acfdir}/www
+cgibindir=${acfdir}/cgi-bin
+appdir=${acfdir}/app
+acflibdir=${acfdir}/lib
+sessionsdir=${localstatedir}/lib/acf/sessions
diff --git a/gnats-controller.lua b/gnats-controller.lua
new file mode 100644
index 0000000..4fa036c
--- /dev/null
+++ b/gnats-controller.lua
@@ -0,0 +1,515 @@
+module(..., package.seeall)
+
+-- Load libraries
+require("format")
+
+-- Set variables
+local newrecordtxt = "[New]"
+local anytxt = "Any"
+local bugreportreceiver = "bugs@alpinelinux.org"
+
+local descr = {
+ labels = {
+ ['category'] = "Category",
+ ['severity'] = "Severity",
+ ['priority'] = "Priority",
+ ['class'] = "Class",
+ ['state'] = "State",
+ ['skipclosed'] = "Skip/Hide closed",
+ ['text'] = "Text in single-line fields",
+ ['multitext'] = "Text in multi-line fields",
+ ['responsible'] = "Responsible",
+ ['originator'] = "Originator",
+ ['release'] = "Alpine release",
+ ['environment']="Environment",
+ ['description']="Description",
+ ['howtorepeat']="How-to-repeat this bug",
+ ['fix']="Fix",
+ ['synopsis']="Short description (on line)",
+ ['from']="Your e-mail address",
+ ['reporter_name']="Your full name",
+ ['reporter_id']="Your ID (Submitter ID)",
+ ['reporter_org']="Your organization name",
+
+ header = {
+ ['received'] = "Received",
+ ['to'] = "To",
+ ['from'] = "From",
+ ['date'] = "Date",
+ ['message_id'] = "Message ID",
+ ['subject'] = "Subject",
+ ['return_path'] = "Return path",
+ },
+ sfields = {
+ ['number'] = "Number",
+ ['class'] = "Class",
+ ['responsible'] = "Responsible",
+ ['category'] = "Category",
+ ['priority'] = "Priority",
+ ['confidential'] = "Confidential",
+ ['originator'] = "Originator",
+ ['severity'] = "Severity",
+ ['keywords'] = "Keywords",
+ ['arrival_date'] = "Arrival date",
+ ['quarter'] = "Quarter",
+ ['synopsis'] = "Synopsis",
+ ['notify_list'] = "Notify list",
+ ['submitter_id'] = "Submitter id",
+ ['date_required'] = "Date required",
+ ['release'] = "Release",
+ ['last_modified'] = "Last modified",
+ ['closed_date'] = "Closed date",
+ ['state'] = "State",
+ },
+ mfields = {
+ ['environment'] = "Environment",
+ ['release_note'] = "Release note",
+ ['audit_trail'] = "Audit trail",
+ ['howtorepeat'] = "How to repeat",
+ ['organization'] = "Organization",
+ ['fix'] = "Fix",
+ ['unformatted'] = "Unformatted",
+ },
+ },
+ audit_trail = {
+ ['Responsible-Changed-From-To'] = "Responsible Changed",
+ ['State-Changed-From-To'] = "State Changed",
+ ['From'] = "Reply via E-mail",
+ },
+ maildescriptions = {
+ ['from'] = "From: ",
+ ['reporter_id'] = ">Submitter-Id: ",
+ ['xxx'] = ">Notify-List: ",
+ ['originator'] = ">Originator: ",
+ ['reporter_org'] = ">Organization: ",
+ ['synopsis'] = ">Synopsis: ",
+ ['xxx'] = ">Confidential: ",
+ ['severity'] = ">Severity: ",
+ ['priority'] = ">Priority: ",
+ ['category'] = ">Category: ",
+ ['class'] = ">Class: ",
+ ['release'] = ">Release: ",
+ ['environment'] = ">Environment: ",
+ ['description'] = ">Description: ",
+ ['howtorepeat'] = ">How-To-Repeat:",
+ ['fix'] = ">Fix: ",
+ },
+
+ }
+
+-- ################################################################################
+-- LOCAL FUNCTIONS
+
+local function list_redir(self)
+ self.conf.action = "status"
+ self.conf.type = "redir"
+ error (self.conf)
+end
+
+
+-- ################################################################################
+-- PUBLIC FUNCTIONS
+mvc = {}
+function mvc.on_load(self, parent)
+ if (self.worker[self.conf.action] == nil ) or ( self.conf.action == "init" ) then
+ self.worker[self.conf.action] = list_redir(self)
+ end
+end
+
+function status(self)
+ return { status=self.model.getstatus() }
+end
+
+function expert(self)
+ local modifications = self.clientdata.filecontent or ""
+ if ( self.clientdata.cmdsave ) then
+ modifications = self.model:update_filecontent(modifications)
+ end
+ local url = ENV["SCRIPT_NAME"] .. self.conf.prefix .. self.conf.controller
+
+ local status=self.model.getstatus()
+ local filedetailsresult, file = self.model:get_filedetails()
+
+ -- Add buttons
+ file.cmdsave = cfe ({
+ name="cmdsave",
+ label="Apply settings",
+ value="Apply",
+ type="submit",
+ })
+ if (self.clientdata.cmdsave) then
+ file.cmdsave.descr="* Changes has been saved!"
+ end
+
+ return ( {
+ option={ script=ENV["SCRIPT_NAME"],
+ prefix=self.conf.prefix,
+ controller = self.conf.controller,
+ action = "update",
+ extra = "" },
+ status = status,
+ file = file,
+ modifications = modifications,
+ url = url, } )
+end
+
+function edit(self)
+
+ -- Save changes
+ local modifications = self.clientdata.filecontent or ""
+ if ( self.clientdata.cmdsave ) then
+ modifications = self.model:update_filecontent(modifications,self.clientdata.name)
+ end
+
+ local status = self.model:getstatus(self)
+ local filedetailsresult, file = self.model:get_filedetails(self.clientdata.name)
+
+
+ if not (filedetailsresult) then
+ self.conf.action = "status"
+ self.conf.type = "redir"
+ error (self.conf)
+ end
+
+ -- Add a cmd button to the view
+ file.cmdsave = cfe({ name="cmdsave",
+ label="Save/Apply above settings",
+ value="Save",
+ type="submit",
+ })
+ if (modifications) then
+ file.cmdsave.descr="* Changes has been saved!"
+ end
+
+ return {
+ option={ script=ENV["SCRIPT_NAME"],
+ prefix=self.conf.prefix,
+ controller = self.conf.controller,
+ action = "edit",
+ extra = "",
+ },
+ modifications = modifications,
+ file = file,
+ status = status,
+ startstop = startstop,
+ }
+end
+
+function query(self)
+ local query = {}
+
+ query.pr = cfe({
+ name="pr",
+ label="Query a specific PR number:",
+ })
+ query.pr_cmd = cfe({
+ name="pr_cmd",
+ label="Submit the query",
+ type="submit",
+ value="Submit",
+ })
+
+ local tags = { "category", "severity", "priority", "class", "state", }
+ for k,val in pairs(tags) do
+ query[val] = cfe({
+ name=val,
+ label=(descr.labels[val] or val),
+ value=anytxt,
+ type="select",
+ option=self.model:get_select_opt(val),
+ })
+ table.insert(query[val].option, 1, anytxt)
+ -- If there is only a limited amount records, then present as radio buttons.
+ if (#query[val].option < 6) then query[val].type="radio" end
+ end
+
+ local val = "responsible"
+ query[val] = cfe({
+ name=val,
+ label=(descr.labels[val] or val),
+ value=anytxt,
+ type="select",
+ option=self.model:list_responsible(),
+ })
+ table.insert(query[val].option, 1, anytxt)
+
+ local tags = { "skipclosed", "text", "multitext", }
+ for k,val in pairs(tags) do
+ query[val] = cfe({
+ name=val,
+ label=(descr.labels[val] or val),
+ option=self.model:get_select_opt(val),
+ })
+ end
+
+ -- Show 'skipclosed' as a checkbox instead
+ query.skipclosed.type="checkbox"
+ query.skipclosed.checked="Yes"
+
+ query.advanced_cmd = cfe({
+ name="advanced_cmd",
+ label="Submit the advanced query",
+ type="submit",
+ value="Submit",
+ })
+
+ return {
+ option={ script=ENV["SCRIPT_NAME"],
+ prefix=self.conf.prefix,
+ controller = self.conf.controller,
+ action = "edit",
+ extra = "",
+ },
+ query = query,
+ }
+end
+
+function queryresult(self)
+ local query = {}
+ local pr_id = self.clientdata.pr
+ if (tonumber(pr_id)) then
+ query.header, query.sfields, query.mfields = self.model:read_pr(pr_id)
+ else
+ list_redir(self)
+ end
+
+ local myform = query.header
+ for k,v in pairs(myform) do
+ myform[k] = cfe({
+ name=k,
+ label=descr.labels.header[k] or k,
+ value=string.gsub(myform[k], "\"", ""),
+ })
+ end
+
+ local myform = query.sfields
+ for k,v in pairs(myform) do
+ myform[k] = cfe({
+ name=k,
+ label=descr.labels.sfields[k] or k,
+ value=string.gsub(myform[k], "\"", ""),
+ })
+ end
+
+ local myform = query.mfields
+ for k,v in pairs(myform) do
+ myform[k] = cfe({
+ name=k,
+ label=descr.labels.mfields[k] or k,
+ value=string.gsub(myform[k], "\"", ""),
+ })
+ end
+
+ local audit_trail_table = {}
+ local checkstring = ""
+ local checkstring2 = 0
+ table.insert(audit_trail_table, cfe({class="", label=""}))
+
+ for k,v in pairs(format.string_to_table(query.mfields.audit_trail.value, "\n")) do
+ if (v == "") then
+ checkstring2 = checkstring2 + 1
+ end
+ if (checkstring2 == 99) then
+ table.insert(audit_trail_table, cfe({class="Unknown", label=""}))
+ checkstring = ""
+ checkstring2 = 0
+ end
+ for k1, v1 in pairs(descr.audit_trail) do
+ if (string.match(v, "^".. string.gsub(k1, "-", "%%-"))) then
+ table.insert(audit_trail_table, cfe({class=k1, label=v1}))
+ checkstring = k1
+ checkstring2 = 0
+ break
+ end
+ end
+ if (#audit_trail_table > 0) then
+ table.insert(audit_trail_table[#audit_trail_table], string.match(v, "^.+")) --Strips empty lines
+ end
+ end
+
+ query.mfields.audit_trail.option=audit_trail_table
+
+ return {
+ option={ script=ENV["SCRIPT_NAME"],
+ prefix=self.conf.prefix,
+ controller = self.conf.controller,
+ action = "edit",
+ extra = "",
+ },
+ query = query,
+ }
+
+end
+
+function summary(self)
+ -- parse the search criterias - select boxes
+ local action = "Query"
+ local s = {}
+ for i, f in ipairs({"class", "category", "severity", "priority", "state", "responsible"}) do
+ s[f] = self.model:validate_field(self.clientdata[f], f)
+ end
+
+ local invalid = nil
+ -- parse the search criterias - input boxes
+ for i,f in ipairs({"text", "multitext", "originator"}) do
+ s[f] = self.model:validate_textfield(self.clientdata[f], f)
+ -- replace this with ajax stuff?
+ if action == "Query" and s[f] == nil then
+ self.model:set_invalid_field(f)
+ invalid = true
+ end
+ -- if field is blank, then ignore it
+ if action == "Query" and s[f] == "" then
+ s[f] = nil
+ end
+ end
+
+ -- we have found atleast one invalid field. reset the action
+ if invalid then
+ action = nil
+ end
+
+ if (self.clientdata.skipclosed) then
+ s["skip-closed"] = ""
+ else
+ s["skip-closed"] = nil
+ end
+
+ -- Set the search criterias
+ self.model:set_search_criteria(s)
+
+ return {
+ option={ script=ENV["SCRIPT_NAME"],
+ prefix=self.conf.prefix,
+ controller = self.conf.controller,
+ action = "edit",
+ extra = "",
+ },
+ summary = self.model:summary(),
+ }
+end
+
+function report(self)
+ local reportform = {}
+ local status = self.model:getstatus(self)
+
+ local alltags = { "reporter_name", "from", "reporter_id", "reporter_org",
+ "synopsis","severity","priority","category","class","release","environment",
+ "description", "howtorepeat", "fix", }
+
+
+ for k,val in pairs(alltags) do
+ reportform[val] = cfe({
+ name=val,
+ label=(descr.labels[val] or val),
+ value=(self.clientdata[val] or ""),
+ })
+ end
+
+ local tags = { "category", "priority","severity","class" }
+ for k,val in pairs(tags) do
+ reportform[val] = cfe({
+ name=val,
+ label=(descr.labels[val] or val),
+ value=(self.clientdata[val] or ""),
+ type="select",
+ option=self.model:get_select_opt(val),
+ })
+ -- If there is only a limited amount records, then present as radio buttons.
+ if (#reportform[val].option < 6) then
+ reportform[val].type="radio"
+ else
+ table.insert(reportform[val].option, 1, "")
+ end
+ end
+
+ reportform.description.type="longtext"
+ reportform.howtorepeat.type="longtext"
+ reportform.fix.type="longtext"
+ reportform.environment.type="longtext"
+ reportform.release.value=status.alpinerelease.value or ""
+
+ -- Add buttons
+ reportform.submit = cfe ({
+ name="submit",
+ label="Submit this repport",
+ value="Submit",
+ type="submit",
+ })
+
+ if (self.clientdata.submit) then
+
+ if not (self.clientdata.from) or (#self.clientdata.from == 0) then
+ reportform.from.errtxt = "You need to enter a valid e-mail address"
+ end
+
+ if not (self.clientdata.synopsis) or (#self.clientdata.synopsis == 0) then
+ reportform.synopsis.errtxt = "You need to enter a valid online-description"
+ end
+
+ if not (self.clientdata.severity) or (#self.clientdata.severity == 0) then
+ reportform.severity.errtxt = "You need to choose one of these options"
+ end
+
+ if not (self.clientdata.priority) or (#self.clientdata.priority == 0) then
+ reportform.priority.errtxt = "You need to choose one of these options"
+ end
+
+ if not (self.clientdata.category) or (#self.clientdata.category == 0) then
+ reportform.category.errtxt = "You need to choose one of these options"
+ end
+
+ if not (self.clientdata.class) or (#self.clientdata.class == 0) then
+ reportform.class.errtxt = "You need to choose one of these options"
+ end
+
+ local noerrorsexists
+ for k,v in pairs(alltags) do
+ noerrorsexists = false
+ if (#reportform[v]['errtxt'] > 0) then break end
+ noerrorsexists = true
+ end
+
+ -- If we had no errors, then we proceed with creating/sending the bugreport
+ if (noerrorsexists) then
+ local bugreport = {}
+ table.insert(bugreport, "To: " .. (bugreportreceiver or ""))
+ table.insert(bugreport, "From: " .. (self.clientdata.from or ""))
+ table.insert(bugreport, "Reply-To: " .. (self.clientdata.from or ""))
+ table.insert(bugreport, "")
+ for k,v in pairs({"reporter_id", "notify_list", "originator", "synopsis", "confidential", "severity",
+ "priority", "category", "class", "release", }) do
+ if (v) and (descr.maildescriptions[v]) and (self.clientdata[v]) then
+ table.insert(bugreport, descr.maildescriptions[v] .. self.clientdata[v] )
+ end
+ end
+ table.insert(bugreport, descr.maildescriptions.environment .. "\n" .. (self.clientdata.environment or "") .."\n" )
+ table.insert(bugreport, descr.maildescriptions.howtorepeat .. "\n" .. (self.clientdata.howtorepeat or "") .."\n" )
+
+ table.insert(bugreport, descr.maildescriptions.fix .. "\n" .. (self.clientdata.fix or "") .."\n" )
+
+ local bugreportsuccess, bugreport = self.model:sendbug(bugreport)
+
+ if (bugreportsuccess) then
+ self.conf.action = "reportsuccess"
+ self.conf.type = "redir"
+ error (self.conf)
+ end
+ reportform.submit.errtxt = "Something went wrong because the report could not be sent.\n" .. bugreport
+ end
+ end
+
+ return {
+ option={ script=ENV["SCRIPT_NAME"],
+ prefix=self.conf.prefix,
+ controller = self.conf.controller,
+ action = "edit",
+ extra = "",
+ },
+ reportform = reportform,
+ }
+end
+
+function reportsuccess()
+ -- Just show that the report was successfully sent.
+end
diff --git a/gnats-edit-html.lsp b/gnats-edit-html.lsp
new file mode 100644
index 0000000..08dee18
--- /dev/null
+++ b/gnats-edit-html.lsp
@@ -0,0 +1,58 @@
+<? local form = ...
+require("viewfunctions")
+?>
+<?
+--[[ DEBUG INFORMATION
+io.write("<H1>DEBUGGING</H1><span style='color:red'><H2>DEBUG INFO: CFE</H2>")
+io.write(html.cfe_unpack(form))
+io.write("</span>")
+--]]
+?>
+
+<H1>SYSTEM INFO</H1>
+<DL>
+<?
+local myform = form.status
+local tags = { "status", "version", "autostart", }
+displayinfo(myform,tags,"viewonly")
+?>
+</DL>
+
+<form name="myform" action="" method="POST">
+<h1>CONFIGURATION</h1>
+<H2>Expert config</H2>
+<h3>File details</h3>
+<DL>
+<?
+local myform = form.file
+local tags = { "filename", "filesize", "mtime", "sumerrors", }
+displayinfo(myform,tags,"viewonly")
+?>
+</DL>
+
+<H3>FILE CONTENT</H3>
+<?
+local myform = form.file
+io.write('<input type="hidden" value="' .. myform.filename.value .. '" name="name">')
+io.write(html.form[myform.filecontent.type](myform.filecontent))
+?>
+
+<H2>SAVE AND APPLY ABOVE SETTINGS</H2>
+<DL>
+<?
+local tags = { "cmdsave", }
+displayinfo(myform,tags)
+?>
+</DL>
+
+<?
+-- Management buttons
+local myform = form.management
+local tags = { "start", "stop", "restart" }
+if (myform) then
+ io.write("<H1>MANAGEMENT</H1>\n<DL>")
+ displaymanagement(myform,tags)
+ io.write("</DL>")
+end
+?>
+</form>
diff --git a/gnats-expert-html.lsp b/gnats-expert-html.lsp
new file mode 100644
index 0000000..da4ac15
--- /dev/null
+++ b/gnats-expert-html.lsp
@@ -0,0 +1,52 @@
+<? local form = ...
+require("viewfunctions")
+?>
+<?
+--[[ DEBUG INFORMATION
+io.write("<H1>DEBUGGING</H1><span style='color:red'><H2>DEBUG INFO: CFE</H2>")
+io.write(html.cfe_unpack(form))
+io.write("</span>")
+--]]
+?>
+
+
+<H1>SYSTEM INFO</H1>
+<DL>
+<?
+local myform = form.status
+local tags = { "status", "version", "autostart", }
+displayinfo(myform,tags,"viewonly")
+?>
+</DL>
+
+<h1>CONFIGURATION</h1>
+<h2>Expert config</h2>
+<h3>List of files</h3>
+<DL>
+<TABLE>
+<?
+local myform = form.status.configfiles
+for k,v in pairs(myform.option) do
+?>
+ <TR>
+ <TD style="padding-right:20px;white-space:nowrap;"><?= html.link{value = "edit?name=" .. v , label=v } ?></TD>
+ </TR>
+<? end ?>
+</TABLE>
+</DL>
+
+
+<form action="<?= form.option.script .. "/" .. form.option.prefix ..
+ form.option.controller .. "/" .. form.option.action ..
+ (form.option.extra or "") ?>" method="POST">
+<?
+-- Management buttons
+local myform = form.management
+local tags = { "start", "stop", "restart" }
+if (myform) then
+ io.write("<H1>MANAGEMENT</H1>\n<DL>")
+ displaymanagement(myform,tags)
+ io.write("</DL>")
+end
+?>
+</form>
diff --git a/gnats-model.lua b/gnats-model.lua
new file mode 100644
index 0000000..28b9d9e
--- /dev/null
+++ b/gnats-model.lua
@@ -0,0 +1,498 @@
+module(..., package.seeall)
+
+-- Load libraries
+require("fs")
+require("procps")
+require("date")
+require("getopts")
+require("format")
+require("posix")
+require("daemoncontrol")
+require("validator")
+require("socket")
+require("processinfo")
+
+-- Set variables
+local configfile = ""
+local processname = "gnats"
+local packagename = "gnats"
+local baseurl = "/etc/gnats" --No trailing /
+local gnatsopts = " -H dev.alpinelinux.org "
+-- constants
+local SECT_HEADER = 1
+local SECT_SFIELDS = 2
+local SECT_MFIELDS = 3
+local header = {}
+local sfields = {}
+local mfields = {}
+local fields_single = {
+ ["number"] = "", ["category"] = "", ["synopsis"] = "",
+ ["confidential"] = "", ["severity"] = "", ["priority"] = "",
+ ["responsible"] = "", ["state"] = "", ["quarter"] = "",
+ ["keywords"] = "", ["date_required"] = "", ["class"] = "",
+ ["submitter_id"] = "", ["arrival_date"] = "", ["closed_date"] = "",
+ ["last_modified"] = "", ["originator"] = "", ["release"] = "",
+ ["notify_list"] = ""
+}
+
+local fields_multiple = {
+ ["organization"]="", ["environment"]="", ["description"]="",
+ ["how_to_repeat"]="", ["fix"]="", ["release_note"]="",
+ ["audit_trail"]="", ["unformatted"]="" }
+
+local descr = {
+}
+
+-- ################################################################################
+-- LOCAL FUNCTIONS
+
+-- Function to recursively inserts all filenames in a dir into an array
+local function recursedir(path, filearray)
+ local k,v
+ for k,v in pairs(posix.dir(path) or {}) do
+ -- Ignore files that begins with a '.'
+ if not string.match(v, "^%.") then
+ local f = path .. "/" .. v
+ -- If subfolder exists, list files in this subfolder
+ if (posix.stat(f).type == "directory") then
+ recursedir(f, filearray)
+ else
+ table.insert(filearray, f)
+ end
+ end
+ end
+end
+
+-- ################################################################################
+-- LOCAL GNATS SPECIFIC FUNCTIONS
+
+local function process_header(line)
+ local k, v
+ if string.find(line, "^%s*$") then
+ return 1
+ end
+
+ k, v = string.match(line, "^([%a%d_.-]+): (.*)$")
+ if k then
+ header[string.lower(k:gsub("-", "_"))] = v
+ end
+ return 0
+end
+
+local function process_sfields(line)
+ local k, v, i, j, key
+ local found = 0
+ key, v = string.match(line, "^>([%a-]+):%s*(.*)$")
+ if key then
+ k = string.lower(key:gsub("-", "_"))
+ if fields_single[k] == nil then
+ return 1
+ end
+ fields_single[k] = v
+ end
+ return 0
+end
+
+local current_mfield = 0
+
+local function mfield_add_line(line)
+ if fields_multiple[current_mfield] == nil then
+ fields_multiple[current_mfield] = line or ""
+ else
+ fields_multiple[current_mfield] = fields_multiple[current_mfield].."\n"..(line or "")
+ end
+end
+
+local function process_mfields(line)
+ local k, v = string.match(line, "^>([%a-]+):%s*(.*)$")
+ if k then
+ k = string.lower(k:gsub("-", "_"))
+ if fields_multiple[k] then
+ current_mfield = k
+ end
+ else
+ v = line
+ end
+ mfield_add_line(v)
+ return 0
+end
+
+local function get_array(name)
+ local a = {}
+ local f = assert(io.popen("/usr/bin/query-pr "..gnatsopts.." --valid-values " .. tostring(name)))
+ for line in f:lines() do
+ table.insert(a, line)
+ end
+ f:close()
+ return a
+end
+
+local function get_various_info()
+ local f = assert(io.popen("/usr/bin/query-pr "..gnatsopts.." -x -q | wc -l"))
+ local count = f:read("*l")
+ f:close()
+ return count
+end
+
+-- ################################################################################
+-- GNATS SPECIFIC FUNCTIONS
+
+
+-- generate an array of select options
+function get_select_opt(self, name)
+ return get_array(name)
+end
+
+-- generate an array of select options
+function list_responsible()
+ local a = {}
+ local f = assert(io.popen("/usr/bin/query-pr "..gnatsopts.." --list-responsible "))
+ for line in f:lines() do
+ table.insert(a, string.match(line,"^(.-):.*$"))
+ end
+ f:close()
+ return a
+end
+
+function summary()
+ -- TODO: this func should return an iterator instead of a table
+ local line
+ local pr = {}
+ local prs = {}
+ local i,k,v
+ local search_opts = ""
+
+ -- create command line options for quer-pr. For example:
+ --
+ -- --priority low --class sw-bug --category doc --skip-closed
+ --
+ -- Note that --skip-closed has value = ""
+ --
+ for k,v in pairs(search_criteria) do
+ if v then
+ search_opts = search_opts.." --"..k.." "..v
+ end
+ end
+
+ local f = assert(io.popen("query-pr "..gnatsopts.. (search_opts or "")))
+
+ i = 0
+ for line in f:lines() do
+ -- if line is empty, insert to array and reset record
+ if line == "" then
+ prs[i] = pr
+ pr = {}
+ end
+ -- extract the key/value:
+ -- >Number: 24
+ -- >Category: pending
+ -- >Synopsis: bug report
+ -- >Confidential: no
+ -- >Severity: serious
+ -- >Priority: medium
+ -- >Responsible: gnats-admin
+ -- >State: open
+ -- >Class: sw-bug
+ -- >Submitter-Id: unknown
+ -- >Arrival-Date: Sat Apr 07 14:45:02 +0000 2007
+ -- >Originator: Nathan Angelacos <nangel@nothome.org>
+ -- >Release:
+ --
+ -- >Number: 25
+ -- ...
+ k,v = line:match(">(.-):%s+(.*)")
+ if k then
+ -- convert to lowercase and replace '-' with '_'
+ --
+ -- Submitter-Id -> submitter_id
+ local key = string.lower(k:gsub("-", "_"))
+ if key == "number" then
+ i = tonumber(v)
+ end
+ if key == "arrival_date" then
+ local datetable = date.string_to_table(v)
+ if (tonumber(datetable.month) < 10) then datetable.month = "0" .. tostring(datetable.month) end
+-- if (tonumber(datetable.day) < 10) then datetable.day = "0" .. tostring(datetable.day) end
+ pr['submit_date'] = string.sub(datetable.year,3) .. "/" .. datetable.month .. "/" .. datetable.day
+ pr['submit_date_in_seconds'] = os.time(datetable)
+ end
+
+ pr[ key ] = v
+ end
+ end
+
+ local prs_sorted = {}
+ for k,v in pairs(prs) do
+ table.insert(prs_sorted, v)
+ end
+ table.sort(prs_sorted, function(a,b) return (a.submit_date_in_seconds > b.submit_date_in_seconds) end)
+
+ return prs_sorted
+end
+
+-- validate if "value" is a valid field
+function validate_field(self, value, field)
+ -- check if for or field is nil
+ if field == nil or value == nil then
+ return nil
+ end
+
+ -- loop through the array
+ for k,v in pairs(get_array(field)) do
+ if value == v then
+ return value
+ end
+ end
+ return nil
+end
+
+-- validate for insecure chars in field
+function validate_textfield(self,value)
+ if value == nil or value == "" then
+ return ""
+ end
+ return string.match(value, "^[-_%a%d@.]*$")
+end
+
+-- just set the private variable
+function set_search_criteria(self, criteria)
+ search_criteria = criteria
+end
+-- append an n valid input message
+function set_invalid_field(self,field)
+ table.insert(invalid_input, field)
+end
+
+--[==[
+-- return a table of invald input fields
+function get_invalid_inputs()
+ return invalid_input
+end
+--]==]
+
+--[=[
+-- dump the pr
+function write_pr(id)
+ local cmd = "query-pr -F "..gnatsopts.." "..tostring(id)
+ local f = assert(io.popen(cmd))
+ io.write(f:read("*all"))
+ f:close()
+end
+--]=]
+-- read pr to header, sfields and mfields
+function read_pr(self, id)
+ local cmd = "query-pr -F "..gnatsopts.." "..tostring(id)
+ local f = assert(io.popen(cmd))
+ local line
+ local section = SECT_HEADER
+
+ for line in f:lines() do
+ -- read header
+ if section == SECT_HEADER then
+ section = section + process_header(line)
+ end
+ if section == SECT_SFIELDS then
+ section = section + process_sfields(line)
+ end
+ if section == SECT_MFIELDS then
+ section = section + process_mfields(line)
+ end
+ end
+ return header, fields_single, fields_multiple
+end
+
+-- ################################################################################
+-- PUBLIC FUNCTIONS
+
+function startstop_service ( self, action )
+ local cmd = action.value
+ local cmdresult,cmdmessage,cmderror,cmdaction = daemoncontrol.daemoncontrol(processname, cmd)
+ action.descr=cmdmessage
+ action.errtxt=cmderror
+ return cmdresult,action
+end
+
+function valid_filename(self, path)
+ local files = {}
+ recursedir(baseurl,files)
+ for k,v in pairs(files) do
+ if (v == path) then
+ return true
+ end
+ end
+ return false, "Not a valid filename!"
+end
+
+function getstatus()
+ local status = {}
+ local config = getconfig()
+ local value, errtxt = processinfo.package_version(packagename)
+ status.version = cfe({ name = "version",
+ label="Program version",
+ value=value,
+ errtxt=errtxt,
+ })
+
+ local files = {}
+ recursedir(baseurl,files)
+ status.configfiles = cfe({ name="configfiles",
+ label="Config files",
+ type="select",
+ descr=table.concat(files,"\n"),
+ option=files,
+ })
+ status.configfiles.size = #status.configfiles.option
+
+ status.numbugs = cfe({ name="numbugs",
+ label="Still not closed reports",
+ value=get_various_info(),
+ })
+
+ local alpinerelease = fs.read_file_as_array("/etc/alpine-release") or {""}
+ status.alpinerelease = cfe({ name="alpinerelease",
+ label="Alpine release",
+ value=alpinerelease[1],
+ })
+
+ status.debug = cfe({ name="debug",
+ label="DEBUG output",
+ value=get_various_info(),
+ })
+
+
+ return status
+end
+
+function get_logfile ()
+ local file = {}
+ local cmdtxt = "cat /var/log/messages | grep " .. processname
+ local cmd, error = io.popen(cmdtxt ,r)
+ local cmdoutput = cmd:read("*a")
+ cmd:close()
+
+ file["filename"] = cfe({
+ name="filename",
+ label="File name",
+ value=cmdtxt,
+ })
+
+ file["filecontent"] = cfe({
+ type="longtext",
+ name="filecontent",
+ label="File content",
+ value=cmdoutput,
+ })
+
+ return file
+end
+
+function getconfig()
+ if (fs.is_file(configfile)) then
+ return getopts.getoptsfromfile(configfile)
+ end
+ return false
+end
+
+function get_filedetails(self,path)
+ if not (valid_filename(self,path)) then
+ return false, {['errtxt'] = "Not a valid filename!"}
+ end
+ local file = {}
+ local filedetails = {}
+ local config = {}
+ local filenameerrtxt
+ if (path) and (fs.is_file(path)) then
+ filedetails = fs.stat(path)
+ config = getconfig(path)
+ else
+ config = {}
+ config.filename = {}
+ config["filename"]["errtxt"]="Config file '".. path .. "' is missing!"
+ end
+
+ file["filename" .. (num or "")] = cfe({
+ name="filename" .. (num or ""),
+ label="File name",
+ value=path,
+ errtxt=filenameerrtxt
+ })
+ file["filesize" .. (num or "")] = cfe({
+ name="filesize" .. (num or ""),
+ label="File size",
+ value=filedetails.size or 0,
+ })
+ file["mtime" .. (num or "")] = cfe({
+ name="mtime" .. (num or ""),
+ label="File date",
+ value=filedetails.mtime or "---",
+ })
+ file["filecontent" .. (num or "")] = cfe({
+ type="longtext",
+ name="filecontent" .. (num or ""),
+ label="File content",
+ value=fs.read_file(path),
+ })
+
+ -- Sum all errors into one cfe
+ local sumerrors = ""
+ for k,v in pairs(config or {}) do
+ if (config[k]) and (config[k]["errtxt"]) and (config[k]["errtxt"] ~= "") then
+ sumerrors = sumerrors .. config[k]["errtxt"] .. "\n"
+ end
+ end
+ if (sumerrors ~= "") then
+ file["sumerrors" .. (num or "")] = cfe ({
+ name="sumerrors" .. (num or ""),
+ label = "Configuration errors",
+ errtxt = string.match(sumerrors, "(.-)\n$"),
+ })
+ end
+
+ return true, file
+end
+function update_filecontent (self, modifications,path)
+ if not (valid_filename(self,path)) then
+ return false, {['errtxt'] = "Not a valid filename!"}
+ end
+ local file_result,err = fs.write_file(path, format.dostounix(modifications))
+ if (err) and (#err > 0) then
+ return false, err
+ end
+ return true, file_result
+end
+
+function sendbug (self, message)
+ if not (message) then
+ return false, "No valid message to send"
+ end
+
+ if not (string.match(string.lower(tostring(message[1])), "^to:")) or
+ not (string.match(string.lower(tostring(message[2])), "^from:")) then
+ return false, "Message is malformatted."
+ end
+
+ local cmdtxt = "/usr/bin/which sendmail"
+ local cmd, error = io.popen(cmdtxt ,r)
+ local cmdoutput = cmd:read("*a")
+ cmd:close()
+ if not (cmdoutput) then
+ return false, "Sendmail is not installed!"
+ end
+
+ local mailtxt = "outgoing_mail"
+ fs.write_file(mailtxt, table.concat(message , "\n"))
+
+ local cmdtxt = "/usr/sbin/sendmail -oi -t < " .. mailtxt .. " 2>&1"
+ local cmd, error = io.popen(cmdtxt ,r)
+ local cmdoutput = cmd:read("*a")
+ cmd:close()
+
+ local cmd, error = io.popen("/bin/rm -f " .. mailtxt ,r)
+ cmd:close()
+
+ if (#cmdoutput > 0) then
+ return false, cmdoutput
+ end
+ return true
+end
+
diff --git a/gnats-query-html.lsp b/gnats-query-html.lsp
new file mode 100644
index 0000000..5aeace6
--- /dev/null
+++ b/gnats-query-html.lsp
@@ -0,0 +1,68 @@
+<? local form = ... ?>
+<?
+--[[ DEBUG INFORMATION
+io.write("<H1>DEBUGGING</H1><span style='color:red'><H2>DEBUG INFO: CFE</H2>")
+io.write(html.cfe_unpack(form))
+io.write("</span>")
+--]]
+?>
+
+<?
+function displayinfo(myform,tags,viewtype)
+ for k,v in pairs(tags) do
+ if (myform[v]) and (myform[v]["value"]) then
+ local val = myform[v]
+ io.write("\n\t<DT")
+ if (#val.errtxt > 0) then
+ val.class = "error"
+ io.write(" class='error'")
+ end
+ io.write(">" .. val.label .. "</DT>")
+ io.write("\n\t\t<DD>")
+ if (viewtype == "viewonly") then
+ if (val.value == "") then val.value = "&nbsp;" end
+ io.write(val.value)
+ elseif (val.type == "radio") and (type(val.option) == "table") and (#val.option > 0) then
+ io.write("<span style='display:inline' class='" .. ( val.class or "") .. "'>")
+ for k1,v1 in pairs(val.option) do
+ io.write("\n\t\t\t"..tostring(v1) .. ":")
+ io.write("<input style='margin-right:20px;margin-left:5px;' type='radio' class='" .. ( val.class or "") .. "' name='" .. val.name .. "'")
+ if (tostring(val.value) == tostring(v1)) then io.write(" checked='yes'") end
+ io.write(" value='" .. v1 .. "'>")
+ end
+ io.write("\n\t\t\t</input></span>")
+ else
+ io.write(html.form[val.type](val))
+ end
+ if (val.descr) and (#val.descr > 0) then io.write("\n\t\t<P CLASS='descr'>" .. string.gsub(val.descr, "\n", "<BR>") .. "</P>") end
+ if (#val.errtxt > 0) then io.write("\n\t\t<P CLASS='error'>" .. string.gsub(val.errtxt, "\n", "<BR>") .. "</P>") end
+ io.write("\n\t\t</DD>\n")
+ end
+ end
+end
+?>
+
+<H1>Query Alpine problem reports</H1>
+
+<form name="cmd" action="queryresult" method="POST">
+<H2>Basic query</H2>
+<DL>
+<?
+local myform = form.query
+local tags = { "pr","pr_cmd", }
+displayinfo(myform,tags)
+?>
+</DL>
+</form>
+
+<form name="cmd" action="summary" method="POST">
+<H2>Advanced query</H2>
+<DL>
+<?
+local myform = form.query
+local tags = { "category", "severity", "priority", "class", "state", "skipclosed", "text", "multitext", "responsible", "originator", "release", "advanced_cmd", }
+displayinfo(myform,tags)
+?>
+</DL>
+</form>
+
diff --git a/gnats-queryresult-html.lsp b/gnats-queryresult-html.lsp
new file mode 100644
index 0000000..c47b919
--- /dev/null
+++ b/gnats-queryresult-html.lsp
@@ -0,0 +1,120 @@
+<? local form = ... ?>
+<?
+--[[ DEBUG INFORMATION
+io.write("<H1>DEBUGGING</H1><span style='color:red'><H2>DEBUG INFO: CFE</H2>")
+io.write(html.cfe_unpack(form.query))
+io.write("</span>")
+--]]
+?>
+
+<?
+function displayinfo(myform,tags,viewtype)
+ for k,v in pairs(tags) do
+ if (myform[v]) and (myform[v]["value"]) then
+ local val = myform[v]
+ io.write("\n\t<DT")
+ if (#val.errtxt > 0) then
+ val.class = "error"
+ io.write(" class='error'")
+ end
+ io.write(">" .. val.label .. "</DT>")
+ io.write("\n\t\t<DD>")
+ if (viewtype == "viewonly") then
+ if not (val.value) or (val.value == "") then val.value = "&nbsp;" end
+ io.write(val.value)
+ elseif (val.type == "radio") and (type(val.option) == "table") and (#val.option > 0) then
+ io.write("<span style='display:inline' class='" .. ( val.class or "") .. "'>")
+ for k1,v1 in pairs(val.option) do
+ io.write("\n\t\t\t"..tostring(v1) .. ":")
+ io.write("<input style='margin-right:20px;margin-left:5px;' type='radio' class='" .. ( val.class or "") .. "' name='" .. val.name .. "'")
+ if (tostring(val.value) == tostring(v1)) then io.write(" checked='yes'") end
+ io.write(" value='" .. v1 .. "'>")
+ end
+ io.write("\n\t\t\t</input></span>")
+ else
+ io.write(html.form[val.type](val))
+ end
+ if (val.descr) and (#val.descr > 0) then io.write("\n\t\t<P CLASS='descr'>" .. string.gsub(val.descr, "\n", "<BR>") .. "</P>") end
+ if (#val.errtxt > 0) then io.write("\n\t\t<P CLASS='error'>" .. string.gsub(val.errtxt, "\n", "<BR>") .. "</P>") end
+ io.write("\n\t\t</DD>\n")
+ end
+ end
+end
+?>
+
+<H1><? io.write(form["query"]["sfields"]["category"]["value"] .. "/")
+io.write(form["query"]["sfields"]["number"]["value"] .. ": " )
+io.write(form["query"]["sfields"]["synopsis"]["value"] or form["query"]["header"]["subject"]["value"] or "Qurery object")?></H1>
+
+<H2>Header</H2>
+<DL>
+<?
+local myform = form.query.header
+local tags = { "from","date", "subject", }
+displayinfo(myform,tags,"viewonly")
+?>
+</DL>
+
+<H2>Details</H2>
+<DL>
+<?
+local myform = form.query.sfields
+local tags = { "class", "release", "state", "priority", "severity", "responsible", "arrival_date", "closed_date", "last_modified", "originator", }
+displayinfo(myform,tags,"viewonly")
+?>
+</DL>
+
+<?
+local myform = form.query.mfields
+local tags = {"organization", "environment", "description", "how_to_repeat", "fix", "release_note", }
+for k,v in pairs(tags) do
+ local myform=myform[v]
+?>
+<H2><? io.write(myform.label) ?></H2>
+<DL>
+<PRE><? io.write(myform.value) ?></PRE>
+</DL>
+<? end ?>
+
+
+<style>
+table {margin:10px;margin-top:5px;border:1px solid #aaa;background:#eee;}
+.Responsible-Changed-From-To {background: #fcc;}
+.Responsible-Changed-From-To {background: #fcc;}
+.State-Changed-From-To {background: #cfc;}
+.From {background: #ccf;}
+.Header {padding:3px;font-weight:bold;border-bottom:1px solid #665;}
+</style>
+
+<?
+local myform = form.query["mfields"]["audit_trail"]
+?>
+<H2><? io.write(myform.label) ?></H2>
+<DL>
+<?
+for k,v in pairs(myform.option) do
+ io.write("\n<TABLE STYLE='width:98%;'>")
+ if (#v.label > 0) then
+ io.write("<TR CLASS='" .. (v.class or "") .. "'><TD COLSPAN=2 CLASS='Header'>" .. (v.label or "Uknown") .. "</TD></TR>")
+ end
+ for i=1,#v do
+ io.write("<TR><TD WIDTH='100%'><PRE>" .. (v[i] or "Uknown") .. "</PRE></TD></TR>")
+ end
+ io.write("\n</TABLE>")
+end
+?>
+</DL>
+
+<?
+local myform = form.query.mfields
+local tags = {"unformatted", }
+for k,v in pairs(tags) do
+ local myform=myform[v]
+?>
+<H2><? io.write(myform.label) ?></H2>
+<DL>
+<PRE><? io.write(myform.value) ?></PRE>
+</DL>
+<? end ?>
+
+
diff --git a/gnats-report-html.lsp b/gnats-report-html.lsp
new file mode 100644
index 0000000..b438fa7
--- /dev/null
+++ b/gnats-report-html.lsp
@@ -0,0 +1,77 @@
+<? local form = ...
+?>
+<?
+--[[ DEBUG INFORMATION
+io.write("<H1>DEBUGGING</H1><span style='color:red'><H2>DEBUG INFO: CFE</H2>")
+io.write(html.cfe_unpack(form))
+io.write("</span>")
+--]]
+?>
+
+<?
+function displayinfo(myform,tags,viewtype)
+ for k,v in pairs(tags) do
+ if (myform[v]) and (myform[v]["value"]) then
+ local val = myform[v]
+ io.write("\n\t<DT")
+ if (#val.errtxt > 0) then
+ val.class = "error"
+ io.write(" class='error'")
+ end
+ io.write(">" .. val.label .. "</DT>")
+ io.write("\n\t\t<DD>")
+ if (viewtype == "viewonly") then
+ if (val.value == "") then val.value = "&nbsp;" end
+ io.write(val.value)
+ elseif (val.type == "radio") and (type(val.option) == "table") and (#val.option > 0) then
+ io.write("<span style='display:inline' class='" .. ( val.class or "") .. "'>")
+ for k1,v1 in pairs(val.option) do
+ io.write("\n\t\t\t"..tostring(v1) .. ":")
+ io.write("<input style='margin-right:20px;margin-left:5px;' type='radio' class='" .. ( val.class or "") .. "' name='" .. val.name .. "'")
+ if (tostring(val.value) == tostring(v1)) then io.write(" checked='yes'") end
+ io.write(" value='" .. v1 .. "'>")
+ end
+ io.write("\n\t\t\t</input></span>")
+ else
+ io.write(html.form[val.type](val))
+ end
+ if (val.descr) and (#val.descr > 0) then io.write("\n\t\t<P CLASS='descr'>" .. string.gsub(val.descr, "\n", "<BR>") .. "</P>") end
+ if (#val.errtxt > 0) then io.write("\n\t\t<P CLASS='error'>" .. string.gsub(val.errtxt, "\n", "<BR>") .. "</P>") end
+ io.write("\n\t\t</DD>\n")
+ end
+ end
+end
+?>
+
+<H1>REPORT A BUG</H1>
+
+<form name="cmd" action="" method="POST">
+
+<H2>Info about you</H2>
+<DL>
+<?
+local myform = form.reportform
+local tags = { "reporter_name", "from", "reporter_id", "reporter_org" }
+displayinfo(myform,tags)
+?>
+</DL>
+
+<H2>Report description</H2>
+<DL>
+<?
+local myform = form.reportform
+local tags = { "synopsis","severity","priority","category","class","release","environment", "description", "howtorepeat", "fix", }
+displayinfo(myform,tags)
+?>
+</DL>
+
+<H2>Submit repport</H2>
+<DL>
+<?
+local myform = form.reportform
+local tags = { "submit", }
+displayinfo(myform,tags)
+?>
+</DL>
+</form>
+
diff --git a/gnats-reportsuccess-html.lsp b/gnats-reportsuccess-html.lsp
new file mode 100644
index 0000000..9f6ba13
--- /dev/null
+++ b/gnats-reportsuccess-html.lsp
@@ -0,0 +1,2 @@
+<H1>Thank you!</H1>
+<p><BR>Your report was successfully sent!<BR><BR></p>
diff --git a/gnats-status-html.lsp b/gnats-status-html.lsp
new file mode 100644
index 0000000..1cf2963
--- /dev/null
+++ b/gnats-status-html.lsp
@@ -0,0 +1,29 @@
+<? local form = ...
+require("viewfunctions")
+?>
+<?
+--[[ DEBUG INFORMATION
+io.write("<H1>DEBUGGING</H1><span style='color:red'><H2>DEBUG INFO: CFE</H2>")
+io.write(html.cfe_unpack(form))
+io.write("</span>")
+--]]
+?>
+
+<H1>SYSTEM INFO</H1>
+<DL>
+<?
+local myform = form.status
+local tags = { "status", "version", }
+displayinfo(myform,tags,"viewonly")
+?>
+</DL>
+
+<H2>PROGRAM SPECIFIC OPTIONS/INFORMATION</H2>
+<DL>
+<?
+local myform = form.status
+local tags = { "numbugs", }
+displayinfo(myform,tags,"viewonly")
+?>
+</DL>
+
diff --git a/gnats-summary-html.lsp b/gnats-summary-html.lsp
new file mode 100644
index 0000000..2c3f84a
--- /dev/null
+++ b/gnats-summary-html.lsp
@@ -0,0 +1,81 @@
+<? local form = ...
+require("viewfunctions")
+?>
+<?
+--[[ DEBUG INFORMATION
+io.write("<H1>DEBUGGING</H1><span style='color:red'><H2>DEBUG INFO: CFE</H2>")
+io.write(html.cfe_unpack(form))
+io.write("</span>")
+--]]
+?>
+
+<style>
+table { margin:10px; border:2px solid #555; }
+table th { border-bottom:2px solid #555; font-weight: bold; }
+table td {border:1px solid #aaa; border-right:none; border-bottom:none; }
+.o { background-color: #fff; padding-bottom: 0px; }
+.a { background-color: #cffafd; padding-bottom: 0px; }
+.f { background-color: #ffc; padding-bottom: 0px; }
+.p { background-color: #d1fbd6; padding-bottom: 0px; }
+.r { background-color: #d6cfc4; padding-bottom: 0px; }
+.s { background-color: #fcccd9; padding-bottom: 0px; }
+.c { background-color: #c1d5db; padding-bottom: 0px; }
+.header {padding:3px;font-weight:bold;border-bottom:1px solid #665;}
+DT {border:1px solid #aaa; width: 40px;}
+DL { padding-top: 5px; }
+</style>
+
+<h1>Current Alpine problem reports</h1>
+<p>The following is a listing of current problems submitted by Alpine users. These represent problem reports covering all versions including experimental development code and obsolete releases. </p>
+<BR>
+
+<h3>Bugs can be in one of several states (S)</h3>
+
+<DL>
+<DT class='o' WIDTH='300px'>o - open</DT><DD>A problem report has been submitted, no sanity checking performed.</DD>
+</DL>
+<DL>
+<DT class='a'>a - analyzed</DT><DD>The problem is understood and a solution is being sought.</DD>
+</DL>
+<DL>
+<DT class='f'>f - feedback</DT><DD>Further work requires additional information from the originator or the community - possibly confirmation of the effectiveness of a proposed solution.</DD>
+</DL>
+<DL>
+<DT class='p'>p - patched</DT><DD>A patch has been committed, but some issues (MFC and / or confirmation from originator) are still open.</DD>
+</DL>
+<DL>
+<DT class='r'>r - repocopy</DT><DD>The resolution of the problem report is dependent on a repocopy operation within the CVS repository which is awaiting completion.</DD>
+</DL>
+<DL>
+<DT class='s'>s - suspended</DT><DD>The problem is not being worked on, due to lack of information or resources. This is a prime candidate for somebody who is looking for a project to do. If the problem cannot be solved at all, it will be closed, rather than suspended.</DD>
+</DL>
+<DL>
+<DT class='c'>c - closed</DT><DD>A problem report is closed when any changes have been integrated, documented, and tested -- or when fixing the problem is abandoned.</DD>
+</DL>
+
+
+<H1>SUMMARY</H1>
+<table style='width:98%;'>
+ <tr class='header'>
+ <th>ID</th>
+ <th>S</th>
+ <th>Submitted</th>
+ <th>Severity</th>
+ <th>Category</th>
+ <th>Description</th>
+ </tr>
+
+<? for k,v in pairs(form.summary) do ?>
+
+ <tr class='<? io.write(string.sub(v.state,1,1)) ?>'>
+ <td width='30px'><A HREF='queryresult?pr=<? io.write(v.number) ?>' STYLE='font-weight:bold;'><? io.write(v.number or "") ?></A></td>
+ <td width='15px'><? io.write(string.sub(v.state,1,1) or "") ?></td>
+ <td width='80px'><? io.write(v.submit_date or "") ?></td>
+ <td width='80px'><? io.write(v.severity or "")?></td>
+ <td width='80px'><? io.write(v.category or "") ?></td>
+ <td style='white-space:normal;word-wrap:break-word'><? io.write(v.synopsis) ?></td>
+ </tr>
+<? end ?>
+
+</table>
+
diff --git a/gnats.menu b/gnats.menu
new file mode 100644
index 0000000..b9c5d6a
--- /dev/null
+++ b/gnats.menu
@@ -0,0 +1,8 @@
+#CAT GROUP/DESC TAB ACTION
+Applications 98GNATS_bugreport Status status
+Applications 98GNATS_bugreport Query query
+Applications 98GNATS_bugreport Report_a_BUG report
+#Applications 98GNATS_bugreport Statuslist summary
+#Applications 98GNATS_bugreport QueryResult queryresult
+Applications 98GNATS_bugreport Configuration expert
+